首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

如何编写测试用例

代码质量管理是软件开发过程中的关键组成部分,比如我们常说的代码规范、代码可读性、单元测试和测试覆盖率等,对于研发人员来说单元测试和测试覆盖率是保障自己所编写代码的质量的重要手段;好的用例可以帮助研发人员确保代码质量和稳定性减少维护成本提高开发效率以及促进团队合作。之前看过一篇关于 OceanBase 质量之道的文章,文章中提到的工程理念就把测试作为非常重要的组成部分,是和研发同样重要的组成部分;也听过内部的同学说过,OB 最核心的是用例。

OceanBase工程理念:经过多年的摸索,OceanBase团队打造了独特的工程文化。测试和开发同时进行,功能测试不再是一个独立分开的过程,而是融入到开发环节,从源端控制引入bug的概率。资深测试人员的精力主要放在难度较大的bug的发现,测试体系建设和相关技术钻研、测试自动化实施。我们建立了一套高效的代码准入流程,防范了许多初级的问题,提升了团队整体的研发效率。

由此可见,测试用例对于项目的重要性。从实际的工作中,也会发现大多数的同学对于如何编写测试用例其实是比较模糊的,在以项目交付为核心思路的工程实践中,测试用例往往只占整个工程周期相当小的一部分,更多时候是依赖测试团队进行功能测试,属于纯黑盒测试。那么这种测试对于业务常规流程可以起到一定的作用,但是对于一些边界问题其实很难 cover 住;另外,基于黑盒模式的功能性测试对于研发团队本身来说,除了拿到准入的测试报告之外,并无其他帮助,当研发需要对代码进行重构或者升级某部分组件时,没有用例的保障,则会将风险直接带到线上环境去。

常见的测试方式

在既往的工作团队中,关于测试方式,包括我自己在内,在没系统了解过测试理论之前,对于各种测试方式也是模棱两可;因为测试方式的种类实在是多又杂。下面是梳理的常见的测试方式,按照不同的维度进行了分类。

每种测试方式都有其独特的目标和方法,可以在软件开发生命周期的不同阶段进行。不同的测试方式在不同的测试维度分类下会有一些重叠,这是正常的,但是他们的关注点是一致的。

在本篇文章中,主要更偏向于研发侧,所以从测试层次角度来看,更多的是关注单元测试(UT)、组件测试(CT)以及集成测试(IT)。总体来说,UT 关注代码单元的正确性,CT关注组件的功能性,IT关注不同组件的集成和协同工作。这些测试层次通常是渐进的,从UT开始,然后是CT,最后是IT。不同的测试方式在软件测试策略中起着不同的作用,这些测试手段的目的就是共同确保软件在各个层次上的质量和稳定性。

下面会通过一个具体的例子来阐述不同的测试方式,主要是针对单元测试、组件测试和集成测试;项目基于 Spingboot 2.4.12 版本,使用 Junit4 和 Mockito 两种测试工具包。

UT、CT 和 IT

在具体看案例之前,先把几个测试工具跑出来,做个简单了解。

测试工具

下面的案例中主要涉及到的测试工具和框架包括:、和。

spring-boot-starter-test

官方文档:https://docs.spring.io/spring-boot/docs/1.5.7.RELEASE/reference/html/boot-features-testing.html

spring-boot-starter-test 是 Spring Boot 提供的一个用于测试的依赖库,它简化了 Spring Boot 应用程序的测试过程,提供了许多有用的工具和类,帮助开发人员编写高效、可靠的单元测试和集成测试。就目前而言,JAVA 技术栈的项目是绕不开 Spring 这套体系的,而绝大多数情况下,在 spring 或者 springBoot 项目中,我们需要依赖 spring 容器刷新之后去测试相应的逻辑,spring-boot-starter-test 就是做这个事情的。

junit4

JUnit 4 是一个用于 Java 编程语言的单元测试框架。目前版本是 JUnit 5,目前我们项目中使用的是 JUnit4。以下是 JUnit 4 中一些常用的特性和概念:

注解驱动的测试:JUnit 4使用注解来标记测试方法,以指定哪些方法应该被运行为测试。常见的测试注解包括 @Test 用于标记测试方法、@Before 用于标记在每个测试方法之前运行的方法、@After 用于标记在每个测试方法之后运行的方法等。对于全局资源的初始化和释放可以通过 @BeforeClass 和 @AfterClass 来搞定。

测试套件:JUnit 4允许你将多个测试类组合在一起,形成一个测试套件,然后可以一次运行所有测试类。这对于组织和管理测试非常有用。

断言:JUnit 4提供了一系列的断言方法,用于验证测试中的条件是否为真。如果条件不满足,断言将引发测试失败。常见的断言方法包括 assertEquals、assertTrue、assertFalse、assertNull、assertNotNull 等。

运行器(Runners):JUnit 4引入了运行器的概念,允许你扩展测试的执行方式。JUnit 4提供了一些内置的运行器,例如 BlockJUnit4ClassRunner 用于普通的 JUnit 测试类,还有一些用于特定用途的运行器,如 Parameterized 用于参数化测试。目前在 springboot 中,使用了 SpringRunner 其实也是 BlockJUnit4ClassRunner 的子类。

关于 Junit 的运行机制可以参考我之前写的一篇文章:你知道 Junit 是怎么跑的吗?

Mockito

Mockito 是一个用于模拟对象的框架,用于创建和配置模拟对象,以模拟外部依赖。Mockito 的主要焦点是模拟外部依赖,以便在单元测试中隔离被测试的代码,并确保它与外部依赖正确交互。和 JUnit 4 的区别在于,JUnit 4  是一个单元测试框架,用于编写和运行测试用例,JUnit 4 的主要焦点是定义和执行测试,以及管理测试生命周期。

关于 Mockito 的运行机制可以参考我之前写的一篇文章:聊一聊 Mockito

单元测试(UT)

在前面的测试分类中,单元测试主要是验证单个代码单元(通常是函数、方法、类等)的正确性;在实际的项目中,单元测试主要是对于一个封装好的工具类的测试。如在 DateUtil 工具类中有一个方法:

右击选中方法,goto -> test,也可以通过相应的快捷键直接创建当前选中方法的测试用例。

image.png

相应的测试代码如下:

这里覆盖了正常的情况,对于传入 date 为 null 的分支并未覆盖到;所以对于强调覆盖率必须满足一定阈值的情况(之前的一个项目中,在 CI 流程中会对当前提供的代码覆盖率进行严格把控,比如行覆盖率比如达到 75% 才能被 merge),则对于不同分支逻辑也需要提供对应的用例。

组件测试(CT)/集成测试(IT)

我们目前基于 SpringBoot test 的测试,大体可以归类于组件测试;这种情况只需要针对当前服务自己的组件进行设计用例;对于可能涉及到的上下游依赖,一般可以通过 mock 的方式来绕过,从而使得当前应用的用例 focus 在自己的业务逻辑上。

使用 mock 代替实际请求

场景描述:;代码如下:

在 HttpUtils 中,底层是对 RestTemplate 的封装:

在上面那段代码中,会具体发送 http 请求到另一个服务去拉取数据。对于这种场景:

1、需要保障用例不会受到对方服务的影响都能顺利执行。

2、关注的是 getUserCaseList 这个方法本身的逻辑(这里举例,代码做了相应的简化)

因此,和实际运行的逻辑不同在于,在编写测试用例时,对于底层发起的 http 调用其实不是主要关注的,可以基于约定好的成功/失败的数据报文结构,通过 mock 的方式来代替实际 http 请求发送。

通过这种形式,则可以有效屏蔽因为三方服务对于我们自己当前用例的影响(核心的还是要关注在自己的业务逻辑上);

准备条件可以在 @Before 中体现

@Before

public void before() {

   RestTemplate restTemplate = Mockito.mock(RestTemplate.class);

   HttpUtils.setRestTemplate(restTemplate);

}

SpringBootTest 说明

在 test_getUserCaseList 中,naturalService 是一个spring bean,因此执行此用例我们需要依赖 spring 容器环境。

@SpringBootTest 在官文档中被描述用于 integration testing 使用的注解,其目的是用于启动一个 ApplicationContext,达到在无需部署应用程序或连接到其他基础设施即可执行集成测试。已上面的代码为例,其中:

webEnvironment 用于描述运行环境,主要包括以下几种类型:

classes 属性用于指定要加载的配置类,这些配置类将用于初始化 Spring Boot 应用程序上下文。通过 classes 属性,可以控制在测试中加载的 Spring Bean 配置,以适应不同的测试需求。在上述案例中,ServerApplication.class 是当前项目的启动类,表示在测试中加载整个应用程序上下文。

@RunWith 用于指定测试运行器(Runner),JUnit 4 默认运行器是 BlockJUnit4ClassRunner ,在 Spring 中对应的是 BlockJUnit4ClassRunner 的子类 SpringJUnit4ClassRunner,而上述代码中的 SpringRunner 和  SpringJUnit4ClassRunner 是一样的,从 SpringRunner 类的源码注释中可以看到,SpringRunner是 SpringJUnit4ClassRunner 的别名()。

使用 H2 内存数据库来代替实际库

在编写用例时,大多数情况下,我们需要依赖数据库的数据进行场景描述;但是一般情况下,即使是测试库,用于作为测试用例的依赖也是不合适的。因此在实践过程中,一般会使用 H2 来代替实际使用的类似 Mysql 数据库来进行测试,实现数据层面的环境隔离。使用 H2 作为测试用例依赖数据库也比较简答,在 pom 中引入如下 H2 的依赖。然后在测试时指定对应 H2 的配置文件代替 Mysql 的配置文件即可。制定配置参考下一小节。

使用指定的测试配置文件

如前面提到,如何我们期望测试用例的环境和实际的环境隔离,则可以使用一个单独的配置文件来描述。比如使用 H2 代替实际的数据库。

测试配置文件 application-test.yaml

指定配置文件

做好测试资源的清理

做好测试资源清理是一个好用例具备的基本前提;如何两个研发同事需要依赖某一个表的数据进行用例设计,如果每个人都没有做好自己用例的资源清理,则在实际的用例执行过程中则会出现用例之间的相互干扰。另外,如过对于一些团队,没有使用 H2 来代替实际的测试库,那么在用例不断执行的过程中,会给测试库产生相当于的测试脏数据。基于上面两个前提,所以我们在设计用例时,特别是涉及到数据或者状态变更的场景时,一定要做好相应的资源清理。如:用户注册的场景逻辑:

在上面这段用例,可能会出现的情况:

如果用户表中没有做基于名字或者手机号的唯一性校验,则在我们的表中可能会出现很多 name 为 test 的用户。(每执行一次,则产生一条记录)

如果用户表做了唯一性约束,那么当第一次执行完之后,第二次执行时则可能会报错,当前用例会执行失败。

所以,在优化这个用例时,就可以将用例执行完之后的数据清除掉。具体做法有两种:

1、在当前用例中执行,比如通过 try finally,在 finally 块中执行删除插入的数据

2、在 @After 中执行删除插入的数据(@After 注解描述的方法,会在每个用例执行完之后执行,通过用于做资源清理)

小结

本篇主要针对如何编写测试用例进行了简单的介绍;包括场景的测试方式分类、测试工具;并通过几个小的测试用例对单元测试、组件测试和集成测试做了分析。最后针对日常研发中,如何做好测试编写和如何做好测试资源释放给了目前主流方案的建议和使用说明。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OZXIUOlOIOW82SQ9hg2yHclQ0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券