使用 Junit + Mockito 实践单元测试
一、前言
相信做过开发的同学,都多多少少写过下面的代码,很长一段时间我一直以为这就是单元测试...
@springboottest @runwith(springrunner.class) public class unittest1 { @autowired private unitservice unitservice; @test public void test() { system.out.println("----------------------"); system.out.println(unitservice.sayhello()); system.out.println("----------------------"); } }
但这是单元测试嘛?unitservice 中可能还依赖了 dao 的操作;如果是微服务,可能还要起注册中心。那么这个“单元”也太大了吧!如果把它称为集成测试,可能更恰当一点,那么有没有可能最小粒度进行单元测试嘛?
单元测试应该是一个带有隔离性的功能测试。在单元测试中,应尽量避免其他类或系统的副作用影响。
单元测试的目标是一小段代码,例如方法或类。方法或类的外部依赖关系应从单元测试中移除,而改为测试框架创建的 mock 对象来替换依赖对象。
单元测试一般由开发人员编写,通过验证或断言目标的一些行为或状态来达到测试的目的。
二、junit 框架
junit 是一个测试框架,它使用注解来标识测试方法。junit 是 github 上托管的一个。
一个 junit 测试指的是一个包含在测试类中的方法,要定义某个方法为测试方法,请使用 @test 注解标注该方法。该方法执行被测代码,可以使用 junit 或另一个 assert 框架提供的 assert 方法来检查预期结果与实际结果是否一致,这些方法调用通常称为断言或断言语句。
public class unittest2 { @test public void test() { string sayhello = "hello world"; assert.assertequals("hello world", sayhello); } }
以下是一些常用的 junit 注解:
注解 | 描述 |
---|---|
@test | 将方法标识为测试方法 |
@before | 在每次测试之前执行。用于准备测试环境(例如,读取输入数据,初始化类) |
@after | 每次测试之后执行。用于清理测试环境(例如,删除临时数据,恢复默认值) |
@beforeclass | 用于 static方法,在所有测试开始之前执行一次。它用于执行耗时的活动,例如:连接到数据库 |
@afterclass | 用于 static方法,在完成所有测试之后,执行一次。它用于执行清理活动,例如:与数据库断开连接 |
@ignore | 指定要忽略的测试 |
@test(expected = exception.class) | 如果该方法未引发命名异常,则失败 |
@test(timeout=100) | 如果该方法花费的时间超过100毫秒,则失败 |
以下是一些常用的 assert 断言:
声明 | 描述 |
---|---|
fail([message]) | 使方法失败。在执行测试代码之前,可用于检查未到达代码的特定部分或测试失败 |
asserttrue([message,]布尔条件) | 检查布尔条件是否为真 |
assertfalse([message,]布尔条件) | 检查布尔条件是否为假 |
assertequals([message,]预期,实际) | 测试两个值是否相同。注意:对于数组,会检查引用而不是数组的内容 |
assertnull([message,]对象) | 检查对象是否为空 |
assertnotnull([message,]对象) | 检查对象是否不为空 |
assertsame([message,]预期,实际) | 检查两个变量是否引用同一对象 |
assertnotsame([message,]预期,实际) | 检查两个变量是否引用了不同的对象 |
三、mockito 框架
从上面的介绍我们可以认识到,如何减少对外部的依赖才是实践单元测试的关键。而这正是 mockito 的使命,mockito 是一个流行的 mock 框架,可以与 junit 结合使用,mockito 允许我们创建和配置 mock 对象,使用 mockito 将大大简化了具有外部依赖项的类的测试开发。spring-boot-starter-test 中默认集成了 mockito,不需要额外引入。
在测试中使用 mockito,通常会:
- mock 外部依赖关系并将 mock 对象插入待测代码
- 执行被测代码
- 验证代码是否正确执行
3.1 使用 mockito 创建 mock 对象
mockit o提供了几种创建 mock 对象的方法:
- 使用静态 mock() 方法
- 使用 @mock 注解
如果使用 @mock 注解,则必须触发创建带有 @mock 注解的对象。使用 mockitorule 可以做到,它通过调用静态方法 mockitoannotations.initmocks(this) 来填充带 @mock 注解的字段。或者可以使用 @runwith(mockitojunitrunner.class)。
public class unittest3 { // 触发创建带有 @mock 注解的对象 @rule public mockitorule mockitorule = mockitojunit.rule(); // 1. 使用 @mock 注解创建 mock 对象 @mock private unitdao unitdao; @test public void test() { // 2. 使用静态 mock() 方法创建 mock 对象 iterator iterator = mock(iterator.class); // when...thenreturn / doreturn...when 模拟依赖调用 when(iterator.next()).thenreturn("hello"); doreturn(1).when(unitdao).delete(anylong()); // 断言 assert.assertequals("hello", iterator.next()); assert.assertequals(new integer(1), unitdao.delete(1l)); } }
3.2 使用 mock 对象实践单元测试
我们要单元测试的内容,常常包含着对数据库的访问等等,那么我们要如何 mock 掉这部分调用呢?我们可以使用 @injectmocks 注解创建实例并使用 mock 对象进行依赖注入。
@service public class unitserviceimpl implements unitservice { @autowired private unitdao unitdao; @override public string sayhello() { integer delete = unitdao.delete(1l); system.out.println(delete); return "hello unit"; } }
@runwith(mockitojunitrunner.class) public class unittest2 { @mock private unitdao unitdao; @injectmocks private unitserviceimpl unitservice; @test public void unittest() { // mock 调用 when(unitdao.delete(anylong())).thenreturn(1); assert.assertequals("hello unit", unitservice.sayhello()); } }
mockito 还有很多有趣的实践,比如:@spy或spy()方法、verify()验证等等,鉴于篇幅原因,读者可自行挖掘。
3.3 使用 powermock mock 静态方法。
mockito 也有一些局限性。例如:不能 mock 静态方法和私有方法。有关详细信息,请参阅 mockito限制的常见问题解答。这个时候我们就要用到 powermock,powermock 支持 junit 和 testng,扩展了 easymock 和 mockito 框架,增加了mock static、final 方法的功能。
首先需要引入 powermock 的依赖:
<!-- powermock --> <dependency> <groupid>org.powermock</groupid> <artifactid>powermock-module-junit4</artifactid> <version>2.0.7</version> <scope>test</scope> </dependency> <dependency> <groupid>org.powermock</groupid> <artifactid>powermock-api-mockito2</artifactid> <version>2.0.7</version> </dependency>
接下来就能愉快的 mock 静态方法了。
@runwith(powermockrunner.class) @preparefortest({stringutils.class}) public class unittest4 { @test public void test() { mockstatic(stringutils.class); when(stringutils.getfilename(anystring())).thenreturn("localhost"); assert.assertequals("localhost", stringutils.getfilename("")); } }
上一篇: Java四种多线程的使用详解
下一篇: C# 基础知识关于反射和泛型