21|什么是测试工序?
你好,我是徐昊,今天我们来继续学习AI时代的软件工程。
上节课,我们按照测试策略的指导,遵循前面讲过的测试驱动开发的节奏,完成了一个简单的功能。这个过程中,我们生成的代码符合项目中既有的架构规则。
我们上节课所采用的方法,就是一个按照测试工序完成编码的例子。今天我们就来讲讲什么是测试工序,以及我们要如何使用它。
测试工序
工序是指完成特定任务或生产产品所需的一系列步骤或程序。在制造业、生产领域或项目管理中,工序通常用于描述完成特定工作的方法或步骤。每个工序都有其独特的目标、方法和所需的资源。例如,在制造产品时,工序可以包括原材料的采购、加工、装配和质量控制等步骤。在项目管理中,工序则可以涵盖项目的规划、执行、监控和收尾等阶段。工序的定义和执行对于确保工作的有效进行和产品的质量至关重要。
而对于软件开发,工序由测试策略定义。正如前一节课学到的,我们按照测试策略的指引,逐步完成架构中不同组件的开发与集成。这个过程中,测试策略制定了我们所需完成的任务。因此,软件开发中的工序也叫测试工序。
让我们再来看一下上节课的例子。
在这个例子中,我们定义了四个测试工序:
- 使用Fake数据库,测试Persistent层中的组件;
- 通过Stub Persistent层的组件,测试Application Logic层中的组件;
- 通过Stub Application Logic层的组件,测试HTTP Interface层中的组件;
- 使用独立的测试数据库,对三层组件进行功能测试。
无论对于哪个需求,我们都能按照上述工序完成。比如,当我们要面对新需求“按SKU商品查询”时,我们可以先按照业务场景,对于商品查询进行场景分解:
- 当查询的商品存在时,返回查到的商品;
- 当查询的商品不存在时,返回404。
然后,我们就可以在不同场景下,按照测试工序对场景进行进一步的分解:
- 当查询的商品存在时,返回查到的商品。
a. Fake数据库,测试从数据库中查询的DAO(工序1);
b. Stub DAO,测试Application Logic(工序2);
c. Stub Application Logic,测试API返回200,以及正确的数据(工序3);
d 功能测试(工序4); - 当查询的商品不存在时,返回404。
a. Fake数据库,测试从数据库中查询的DAO(工序1,同查询商品存在时的工序1);
b. Stub DAO,测试Application Logic(工序2);
c. Stub Application Logic,测试API返回404(工序3);
d. 功能测试(工序4);
对于其他需求,我们也可以进行类似的分解。需要注意的是,虽然我们最终完成了架构中规定的组件,但我们主要是按照测试策略划分的工序。同样对于上面的例子,如果我们的测试策略发生了改变。比如,因为测试成本或是因为所选用的工具/框架存在特殊的限制,我们改变了测试策略:
在新的测试策略下,我们定义了三个测试工序:
- 使用Fake数据库,测试Persistent层中的组件;
- 通过Stub Persistent层的组件,测试HTTP Interface层和Application Logic层中的组件;
- 使用独立的测试数据库,对三层组件进行功能测试。
那么,当我们要面对新需求“按SKU商品查询”时,我们的任务分解就变成了这个样子:
1.当查询的商品存在时,返回查到的商品。
a. Fake数据库,测试从数据库中查询的DAO(工序1);
b. Stub DAO,测试API返回200,以及正确的数据(工序2);
c. 功能测试(工序3);
2.当查询的商品不存在时,返回404。
a. Fake数据库,测试从数据库中查询的DAO(工序1,同1a);
b. Stub DAO,测试API返回404(工序2);
c. 功能测试(工序3);
可以看到虽然架构中规定的组件没有改变,但是因为选择了不同的测试策略,就会到得完全不同的工序。
看到这里,你可能会有疑问,为什么我们不直接从架构上去设计工序,而要依赖于测试策略?这是因为,对于今日的软件开发,可测试性是进程内架构最重要的属性。
可测试性是进程内架构的最重要属性
大部分进程内架构模式的引入,都意味着在可测试性上的改进。下面让我们看一个例子。
以前端开发为例,假设我们现在不使用任何框架,直接使用JavaScript和HTML进行开发。我们默认使用的架构模式是MVC(Model-View-Controller)架构模式。它将应用程序分成三个主要部分:
- 模型(Model):模型代表应用程序的数据和业务逻辑。它负责处理数据的存储、检索、更新和验证。在MVC架构中,模型通常独立于用户界面,这意味着它可以用于不同的用户界面或应用程序;
- 视图(View):视图是用户界面的呈现部分,负责向用户显示数据,并接收用户的输入。它通常包括用户界面元素,如按钮、文本框、图表等。视图负责呈现模型的数据,但不直接处理数据。对于我们的应用而言,视图就是HTML DOM;
- 控制器(Controller):控制器是模型和视图之间的中介。它会接收用户的输入(通常来自视图),然后根据输入更新模型或者调用适当的视图来呈现数据。控制器负责应用程序的流程控制和业务逻辑。对于我们的应用而言,控制器就是挂在HTML DOM上,监听并响应事件的事件处理逻辑。
由于HTML DOM和它上面的事件都难以测试,从实际情况出发,很多团队选择不去对它们进行测试。那么我们当前架构下的测试策略可能是这样的:
这个测试策略显然存在很大的隐患,容易出错。虽然功能测试覆盖(Q2)了大量代码聚集的视图和控制器部分,但没有考虑到针对前端的功能测试成本很高(运行慢,测试不稳定等等)。Q1测试过于集中在不易出错且容易测试的模型部分,对于前端反而缺少有效的Q1测试来降低成本,提高定位准确性。因而整体测试的投资回报率不高。
一个很容易想到的改进办法,就是使用MVP(Model-View—Presenter)架构模式,来改造这个应用。MVP架构模式是一种软件设计模式,用于开发用户界面。它是一种演变自经典MVC(Model-View-Controller)模式的设计范式。MVP模式的核心组成部分包括:
- 模型(Model):模型代表应用程序的数据和业务逻辑。它负责处理数据的存储、检索和修改,以及应用程序的核心功能。
- 视图(View):视图是用户界面的展示部分,负责展示数据并向用户传达信息。它通常是由用户直接与之交互的部分。
- Presenter:Presenter是模型和视图之间的中介,负责处理用户输入、更新模型数据并更新视图。Presenter从模型中检索数据,并将其转换为视图可以理解和展示的形式。Presenter还接收来自视图的用户输入,对其进行处理并相应地更新模型。
因而我们可以得到新的架构:
MVP架构模式引入了不同的组件,我们就有不同的测试策略可以选择了。通常而言,对于视图(View)的测试成本很高。一种流行的测试策略是,忽略对于视图的测试,将测试的重点集中在Presenter上。毕竟Presenter中封装的是交互逻辑。那么,我们可以通过Stub或使用Fake的Model,对Presenter进行测试。于是我们可以得到以下的测试策略:
对比MVC的架构模式,我们缩小了没有Q1测试覆盖的组件范围(View+Controllers 到View)。使Q1测试更多覆盖到逻辑密集的部分(Presenter)。单从测试的角度上来看,就能知道,在当前情况下,使用MVP架构模式能更好地提高交付质量。
当然,我们还可以更进一步。针对视图不好测试的问题,行业也在努力寻找解决方案,也出现了新的工具,比如类似于React Testing Library,Storybook Component Test或者Ladle等等。
那么,当我们选择使用这些工具时,我们可以将测试的重点放在View和Presenter上,通过Stub或Fake Model模拟不同的场景,完成对于View和Presenter的测试,并以此代替功能测试。于是我们可以得到以下的测试策略:
这显然是一个投资回报率更好的测试策略。因而,我们不需要考虑那么多虚妄的 *-abilities,比如可扩展性等等。单从可测试性角度上来看,我们就能知道,在当前的场景下,MVP是比MVC更好的架构模式。而这些,都能从测试策略的改变上发现端倪。
小结
工序是架构落地的重要手段。架构设计通常是在更高层次上进行的,它定义了系统的结构和组件之间的关系,以及如何满足系统的需求和非功能性属性。然而,要使得架构设计真正生效,需要通过具体的步骤和方法将其实现并落地到实际的开发过程中,而工序就是实现这一目标的关键手段之一。
工序定义了完成特定任务或生产产品所需的一系列步骤或程序。在软件开发中,工序可以指导开发团队按照预定的流程和方法进行开发工作,确保架构设计的理念和原则得以贯彻执行。通过合理的工序,开发团队可以有序地进行系统开发、集成、测试和部署,从而确保最终交付的软件系统符合设计要求、具备良好的质量和可靠性。
工序在架构落地过程中扮演着重要的角色,它有助于将抽象的架构设计转化为具体的开发任务和实际的工作流程,帮助团队有效地实现架构设计,并最终产生质量高、可靠性强的软件系统。
下节课,我们将会学习如何使用LLM帮助我们应用工序。
思考题
请将你团队中的架构,按照测试策略转化为工序。
欢迎在留言区分享你的想法。