文章目录
1、什么是单元测试
单元测试是一种白盒测试,测试者依据程序的内部结构来实现测试代码。
单元测试英文单词: Unit Test 。
什么是 Unit (单元)?
单元可以是一个方法、可以是一个类,也可以是一个包甚至是一个子系统。
我们开发时编写的单元测试,通常是对一个类中的部分或者所有方法进行测试,用来验证它们功能的正确性。
通常用来验证给定特定的输入,是否能够给出符合预期的输出。
2、为什么要写单元测试
- 降低测试成本,减少BUG,快速定位BUG
- 模拟各种异常、各个分支场景
- 单元测试可以确保单个方法按照正确预期运行,如果修改了某个方法的代码,只需确保其对应的单元测试通过,即可认为改动正确
- 单元测试代码可以作为示例代码,用来演示如何调用该方法,有助于理解代码的逻辑
3、传统main方法测试
传统main方法测试缺点:
- 只能有一个main()方法,不能把测试代码分离
- 没有打印出测试结果和期望结果,例如,expected: 2, but actual: 1
- 很难编写一组通用的测试代码
4、如何编写测试
宏观角度
宏观角度单元测试要符合AIR原则
- A: Automatic (自动化):
A:单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的, 执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。 单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。
- I: Independent (独立性)
I:保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之 间决不能互相调用,也不能依赖执行的先后次序。 反例:method2 需要依赖 method1 的执行,将执行结果作为 method2 的输入。
- R: Repeatable (可重复)
R:单元测试是可以重复执行的,不能受到外界环境的影响。 说明:单元测试通常会被放到持续集成中,每次有代码 check in 时单元测试都会被执行。如果单测对外部 环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。 正例:为了不受外界环境影响,要求设计代码时就把 SUT 的依赖改成注入,在测试时用 spring 这样的 DI 框架注入一个本地(内存)实现或者 Mock 实现。
微观角度
微观角度从单元测试的代码层面要符合BCDE原则
- B: Border,边界值测试,包括循环边界、特殊取值、特殊时间点,数据顺序
- C: Correct,正确的输入,并得到预期的结果
- D: Design,与设计文档相结合,来编写单元测试
- E: Error,单元测试的目的是证明程序有错,而不是证明程序无错。为了发现代码中潜在的错误,我们需要在编写测试用例时有一些强制的错误输入(如非法数据、异常流程、非业务允许输入等)来得到预期的错误结果
5、Junit常用注解
注意: 这里说的都是Junit4 的注解,Junit5有些变化
常用的JUnit注解包括@BeforeClass、 @AfterClass、 @Before、 @After、 @Test、 @lgnore等。
它们得到了每个测试用例的运行次序,即:@BeforeClass - >@Before - >@Test - >@After - >@AfterClass,从而确定了整个测试流程。
注解 | 描述 |
---|---|
@Test | 将方法标识为测试方法 |
@Before | 在每次测试之前执行。用于准备测试环境(例如,读取输入数据,初始化类) |
@After | 每次测试之后执行。用于清理测试环境(例如,删除临时数据,恢复默认值) |
@BeforeClass | 用于 static方法,在所有测试开始之前执行一次。它用于执行耗时的活动,例如:连接到数据库 |
@AfterClass | 用于 static方法,在完成所有测试之后,执行一次。它用于执行清理活动,例如:与数据库断开连接 |
@Ignore | 指定要忽略的测试 |
@Test(expected = Exception.class) | 如果该方法未引发命名异常,则失败 |
@Test(timeout=100) | 如果该方法花费的时间超过100毫秒,则失败 |
Method | Description |
---|---|
assertNull(java.lang.Object object) | 检查对象是否为空 |
assertNotNull(java.lang.Object object) | 检查对象是否不为空 |
assertEquals(long expected, long actual) | 检查long类型的值是否相等 |
assertEquals(double expected, double actual, double delta) | 检查指定精度的double值是否相等 |
assertFalse(boolean condition) | 检查条件是否为假 |
assertTrue(boolean condition) | 检查条件是否为真 |
assertSame(java.lang.Object expected, java.lang.Object actual) | 检查两个对象引用是否引用同一对象(即对象是否相等) |
assertNotSame(java.lang.Object unexpected, java.lang.Object actual) | 检查两个对象引用是否不引用统一对象(即对象不等) |
6、Mock场景和意义
场景
- 网络交互,调用外部系统接口,如果两个被测模块之间是通过网络进行交互的 被测试模块需要和一些不容易构造、比较复杂的对象进行交互
- 外部资源,比如文件系统。如果被测对象对此类外部资源依赖性非常强,而其行为的不可预测性很可能导致测试的随机失败,此类的外部资源也适合进行
- 其它的协同模块尚未开发完成
- 由于不能肯定其它模块的正确性,我们也无法确定测试中发现的问题是由哪个模块引起的
意义
- 隔离测试对象对外部服务的依赖(比如数据库,第三方接口等),使得测试用例可以独立运行。不管是传统的单体应用,还是现在流行的微服务,这点都特别重要,因为任何外部依赖的存在都会极大的限制测试用例的可迁移性和稳定性。
- Mock可以替换外部服务调用,提升测试用例的运行速度。任何外部服务调用至少是跨进程级别的消耗,甚至是跨系统、跨网络的消耗,而Mock可以把消耗降低到进程内。比如原来一次秒级的网络请求,通过Mock可以降至毫秒级,整整3个数量级的差别。
7、Mock原理
Mock就是做一个假的object,对这个object里的方法的调用,都会被Mockito拦截,然后返回用户预设的行为。这样可以绕过需要从其它地方拿数据的地方,直接返回用户预设的数据,进行单元测试。
Mocktio原理
通过bytebuddy(操作字节码的框架)生成子类的方式,使用方法拦截器,拦截具体的方法调用,将方法调用转发至Mock的桩代码,从而实现方法返回值或者方法执行体的自定义。
注意:它不能 mock 被 final/private 修饰的方法,被final修饰的类。
8、Mockito注解
- @RunWith(MockitoJUnitRunner.class):测试类上的注解,启动注解
- @InjectMocks:将mock对象注入
- @Mock :mock对象,打桩方法,返回打桩值。其余方法不调用真实方法,返回默认值
- @Spy :mock对象,打桩方法,返回打桩值。其余方法调用真实方法
- @MockBean: 功能同 @Mock, 只是会将实例放入 Spring 容器管理
- @SpyBean: 功能同 @Spy, 只是会将实例放入 Spring 容器管理
- @Captor:创建字段级参数捕获器。它与Mockito的verify()方法一起使用, 以获取调用方法时传递的值。
9、Mockito三步骤
在测试中使用 Mockito,通常会:
- mock 外部依赖关系并将 mock 对象插入待测代码
- 执行被测代码
- 验证代码是否正确执行
10、遇到的一些问题
- mock静态类没有清理
- verify(mock,times())没有使用clearInvocations清理
- mock对象的方法有多个参数,如果对某个参数使用any…()/eq()方法匹配,则所有参数都需要使用any…()/eq()匹配,而不能使用真实对象作为参数。