《精通Spring4.x 企业应用开发实战》第20章 实战型单元测试

2023-11-19

前言

TestNG是必须事先掌握的基础测试框架,大多数测试框架和测试工具都在此基础上扩展而来。Spring测试框架为集成 TestNG、JUnit 等单元测试框架提供了很好的支持,并为测试 Spring 应用提供了许多基础设施。
在项目单元测试过程中,不可避免地要涉及测试环境准备,如模拟接口测试测试数据准备等繁杂工作。Mockito、 Unitils、 DbUnit 等框架的出现,使得这些问题有了很好的解决方案,特别是 Unitils 结合 DbUnit 对测试 DAO 层提供了强大的支持,大大提高了编写测试用例的效率和质量。

20.1 单元测试概述

按照软件工程思想,软件测试可以分为单元测试、集成测试、功能测试、系统测试等。功能测试和系统测试一般来说是测试人员的职责,但单元测试集成测试则必须由开发人员保证。

  • 单元测试:当对 UserService 这个业务层的类进行单元测试时,可以通过创建UserDao 和 LoginLogDao** 模拟对象**,在假设 DAO 类正确工作的情况下对 UserService 进行测试。
  • 集成测试:而对 UserService 进行集成测试时,则应该注入真实的 UserDao 和 LoginLogDao进行测试。

所以,一般来讲,集成测试面向的层面要更高一些,一般对业务层和 Web层进行集成测试:单元测试则面向一些功能单一的类(如字符串格式化工具类、数据计算类)。

20.2 TestNG 快速进阶

20.2.1 TestNG 概述

TestNG是一个设计用来简化广泛的测试需求的测试框架,其灵感来自 JUnit 和NUnit,但引入了一些新的功能,使其功能更强大,使用更方便。TestNG 添加了诸如灵活的装置、测试分类、参数测试、依赖方法、数据驱动等特性,让开发人员编写测试更加灵活、简便。
TestNG 设计的出发点是不仅可以用于单元测试,而且可以用于集成测试。相比于JUnit 只适合单元测试,TestNG 无疑会走得更远。
编写一个测试的过程有3个典型步骤。

  1. 编写测试的业务逻辑并在代码中插入 TestNG 注解。
  2. 将测试信息添加到 testng.xml 或者 build.xml 文件中。
  3. 运行 TestNG。

20.2.2 TestNG 生命周期

TestNG 测试用例的完整生命周期要经历以下阶段:类级初始化资源处理、方法级初始化资源处理、执行测试用例中的方法、方法级销毁资源处理、类级销毁资源处理

  • 类级初始化、销毁资源处理方法在一个测试用例类中只能运行一次
  • 方法级初始化、销毁资源处理方法在执行测试用例类的每个测试方法中都会运行一次,以防止测试方法相互之间的影响。测试用例的执行过程如图20-3所示。

图片.png

20.2.3 使用 TestNG

1. 测试方法

在TestNG 中使用@Test 注解来标注一个测试方法。此外可以采用 Java 5.0 的静态导入功能导入断言 Assert 类,这样就可以很方便地在测试方法中使用断言方法。下面通过一个实例来快速体验 TestNG 测试方法,如代码清单20-1 所示。
图片.png
图片.png

在 TestNG 中,只要在每个测试方法中添加@Test 注解即可。像②处的 assertYyy0断言方法就是测试 Money 的 add0方法功能运行正确性的测试规则。
可以在 MoneyTest 中添加多个测试方法,运行器会为每个方法生成一个测试用例实例并分别运行

2. @BeforeClass和@AfterClass

在 TestNG 中加入了两个注解:@BeforeClass 和@AfterClass,使用这两个注解的方法,在一个 Test 类的所有测试方法执行前后 各执行一次。这是为了能在 @BeforeClass中初始化一些昂贵的资源,如数据库连接,然后执行所有的测试方法,最后在@AfterClass中释放资源。对于初学者来讲,很容易混淆@BeforeClass/@AfterClass 与@BeforeMethod/@AfterMethod,为此表20-1 对它们作了一下对比。

图片.png3. 异常测试

因为使用了注解特性,所以 TestNG 测试异常非常简单明了。通过对@Test 传入expected 参数值,即可测试异常。在传入异常类后,测试类如果没有抛出异常或者抛出一个不同的异常,本测试方法就将失败。如代码清单 20-2 所示为一个简单的异常测试实例。
图片.png

4. 超时测试

通过在@Test 注解中为 timeOut 参数指定时间值,即可进行超时测试。如果测试运行时间超过指定的毫秒数,则测试失败。超时测试对网络链接类非常重要。通过 timeOut进行超时测试非常简单,如代码清单20-3 所示为一个简单的超时测试实例。
图片.png

5. 参数化测试

为了测试程序的健壮性,可能需要模拟不同的参数对方法进行测试。如果为每个类型的参数创建一个测试方法,则是一件很难接受的事。幸好 TestNG 提供了参数化测试,它能够创建由参数值供给的通用测试,从而为每个参数都运行一次,而不必创建多个测试方法,如代码清单20-4 所示。
图片.png
首先编写测试类的参数数据提供者方法,然后用此方法进行参数初始化。该方法返回一个 Objectl()类型。用@DataProvider 注解来标注该方法,并设置 name 值。之后在需要测试的方法中设置@Test 的 dataProvider 属性,其值要和上面@DataProvider 修饰的方法中的 name 值保持一致。

6. 分组测试

TestNG 支持执行复杂的分组测试。不仅可以声明单个测试用例内的测试方法的分组,而且还可以声明不同测试用例类级的分组。在执行 TestNG 测试时,可以指定要执行的测试分组及排除不执行的测试分组,如代码清单 20-5 所示
图片.png
图片.png

7. 依赖测试

有些时候,我们需要测试方法按照一个特定的顺序被调用。这非常有用,比如,在运行某个测试方法前需要先运行特定的测试方法,或希望初始化方法也作为测试方法(因为被标注为@BeforeXXX/@AfterXXX 的方法不作为测试报告的一部分)。为了实现这些需求,TesING 为@Test 注解提供了 dependsOnMethods 或 dependsOnGroups 属性来实现测试方法间的依赖关系,如代码清单 20-6 所示。
图片.png
测试方法 testMethod3()设置了两个依赖的测试方法 testMethod1()及 testMethod2(),当执行 testMethod3()测试方法时,会先调用两个依赖的测试方法。如果 testMethodl()或testMethod2()中的任何一个方法测试失败,则 testMethod3()将不被执行。

  • 只有依赖的方法测试全部通过时,当前方法才会被调用执行,这种依赖关系被称为强依赖,也是 TestNG默认的依赖关系
  • 可以通过@Test 提供的 alwaysRun 属性来改变这种强依赖,如示例中改成@Test(dependsOnMethods= {" testMethodl"," testMethod2",alwaysRun=true)之后,不管测试方法 testMethod1l(或 testMethod2()有没有通过测试,testMethod3()总会被执行,这种依赖关系被称为软依赖。

20.3 模拟利器 Mockito

20.3.1 模拟测试概述

目前支持 Java 语言的 Mock 测试工具有 EasyMock、 JMock、 Mockito、 MockCreator、Mockrunner、 MockMaker 等,其中 Mockito 是一个针对 Java 的 Mocking框架。
在 Mockito 中,when(⋯).thenReturn(⋯)这样的语法便是设置方法调用的返回值。另外也可以设置方法在何时调用会抛出异常等。Mock 对象用来验证测试中所依赖对象间的交互是否能够达到预期。在 Mockito 中用 verify….methodXxx(…))语法来验证 methodXxx()方法是否按照预期进行了调用。

20.3.2 创建 Mock 对象

可以对类和接口进行 Mock 对象的创建,创建的时候可以为 Mock 对象命名,也可以忽略命名参数。为 Mock 对象命名的好处就是调试的时候会很方便。比如,我们 Mock多个对象,在测试失败的信息中会把有问题的 Mock 对象打印出来,有了名字就可以行容易地定位和辨认出是哪个 Mock 对象出现了问题。
另外,其使用也有限制,对于 final类、匿名类和 Java 的基本类型是无法进行 Mock 的。除了用 Mock 方法来创建模拟对象,如 mock(Class classToMock),也可以使用**@Mock 注解**定义 Mock。下面通过实例来介绍一下如何创建一个 Mock 对象,如代码清单 20-7 所示。
图片.png
1.在①处和②处,通过 Mockito 提供的 mock()方法创建 UserService 用户服务接口、用户服务实现类 UserServicelmpl 的模拟对象。
2.在③处,通过@Mock 注解创建用户 User类的模拟对象,并且需要在测试类初始化方法中,通过 MockitoAnnotations.initMocks()方法初始化当前测试类中所有标注@Mock 注解的模拟对象。如果没有执行这一步初始化操作,则测试时会报模拟对象为空对象异常。

20.3.3 设定 Mock 对象的期望行为及返回值

when…then/do…when

从上文中我们己经知道,可以通过 when(mock.some Method().thenReturn(value)来设定 Mock 对象的某个方法调用时的返回值,但它同样有限制条件,即对于 static 和 final 修饰的方法是无法进行设定的。下面通过实例来介绍一下如何调用方法及设定返回值,如代码清单20-8 所示。
图片.png

given-有返回值

图片.png

BDDMockito extends Mockito,given是BDDMockito专属的。

doNothing无返回值

图片.png

概念区分

@Mock、Mockito.mock()、@MockBean的区别

1、Mockito.mock()和@Mock的区别
相同点:

  1. 不需要启动spring容器;
  2. Mockito.mock()和@Mock的作用都是生成一个接口或者类的mock对象。

不同点:@Mock注解生效必须使用@RunWith(MockitoJUnitRunner.class)或者MockitoAnnotations.openMocks()去初始化对象,否则直接使用@Mock的对象会报空指针。
MockitoAnnotations.initMocks()已经被MockitoAnnotations.openMocks()替代。

// @Mock注解生效的两种方式,使用以下一种即可
// 方式一:使用@RunWith(MockitoJUnitRunner.class)
@RunWith(MockitoJUnitRunner.class)
public class Test {
    @Mock
    private HttpServletReqeust reqeust;
    
    // 方式二:使用MockitoAnnotation.openMocks(this)
    @Before
    public void init() {
        MockitoAnnotation.openMocks(this); // 初始化@Mock注解的对象
    }
}

2、 @MockBean的作用

  1. 必须启动spring容器;
  2. 如果测试中需要用到一部分不想Mock的类,比如Mapper、公共判断等的这些类,则可以启动Spring容器。那些不想要Mock的bean让容器autowire注入;那些想Mock此时不能用注解@Mock,只能用@MockBean。
  3. @mockBean注解将Mock对象添加到Spring上下文中。注解的对象将替换Spring上下文中任何相同类型的现有bean,如果没有定义相同类型的bean,将添加一个新的bean。

总结:
如果测试中不需要用到容器中的东西,即所有都可以Mock注入,那Mock够用了,大部分也够…
如果测试中需要用到一部分不想Mock的类,比如Mapper,公共判断等的这些类,则可以启动Spring容器,那些不想要Mock的直接不管,让容器注入,那些想Mock此时不能用注解@Mock,只能用@MockBean。
参考:

@Mock 和 @InjectMocks区别

  • @Mock: 创建一个Mock.

  • @InjectMocks: 创建一个实例,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中

     注意:必须使用@RunWith(MockitoJUnitRunner.class) 或 Mockito.initMocks(this)进行mocks的初始化和注入。
    
@RunWith(MockitoJUnitRunner.class)
public class Test {
 
    @InjectMocks
    private SomeHandler someHandler;
 
    @Mock
    private OneDependency oneDependency; // 此mock将被注入到someHandler
 
    // ... 
 
}

xxx

超类声明

超类在BeforeClass中配置了‘Spring上下文’ 。 在容器启动的时候,会加载上下文,有且只有一次加载。
虽然被设置在BeforeClass中(每个测试类启动的时候都执行一次),但是由于applicationContext地址不变,所以每个测试类中的spring上下文也是不变的
图片.png

猜测与验证1-MockBean与Autowired

猜测:
子类RouteControlRestTest @MockBean了 MailboxServerAccountRepository,
子类MailEventRecordServiceTest @Autowired了 MailboxServerAccountRepository,
我猜测:两个测试类中的MailboxServerAccountRepository对象地址不一样
图片.png

验证:

子类-RouteControlRestTest:
图片.png

输出:mailboxServerAccountRepository bean

子类-MailEventRecordServiceTest:
图片.png

输出:com.atta.infra.edmmailserv.component.repository.MailboxServerAccountRepository@2ce5ea69

猜测与验证2-两个Autowired

猜测:
两个子类中MailboxServerAccountRepository地址一样。
图片.png
验证:
子类-RouteControlRestTest:
图片.png

输出:com.atta.infra.edmmailserv.component.repository.MailboxServerAccountRepository@2ce5ea69

子类-MailEventRecordServiceTest:
图片.png

输出:com.atta.infra.edmmailserv.component.repository.MailboxServerAccountRepository@2ce5ea69

猜测与验证3-两个MockBean

猜测:
两个子类中MailboxServerAccountRepository地址一样。
图片.png
验证:
地址都是mailboxServerAccountRepository bean。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

《精通Spring4.x 企业应用开发实战》第20章 实战型单元测试 的相关文章

随机推荐