Part1 前言
UT的作用:
- 帮助发现bug
- 放心重构
- 边界条件
通过此篇文章,可以学到如下内容:
- UT规范
- Mock
- 断言
Part2 开发者测试
FIRST原则
F:Fast,测试执行要快
如单条测试用例执行时间<1s;单个测试文件执行<1min
I:Independent,独立
- 每个测试用例都可以单独执行,不依赖其他任何用例,用例执行结果与顺序无关
- 测试用例之间不能相互调用
- 用例所需的初始化条件/数据在本用例中预置,不依赖用例的执行顺序
- 用例不能修改全局变量/状态,防止用例间的全局变量相互影响,导致用例随机失败
R:Repeatable,可重复
测试程序在不同环境可重复运行,且每次测试结果相同
S:Self-Validating,自校验
测试结果应该明确,无须人工检查来判断结果是否正确。(断言)
T:Timely,及时
单元测试代码和生产代码同步输出,及时验证,及时进行review,不要后续为了覆盖率而补用例。(TDD)
规范
目录结构
// UT代码目录
src/test/java
// UT资源文件目录
src/test/resources
用例命名
- 下划线方式:test_xxx_when_xxx_then_xxx
- 驼峰方式:testXxxWhenXxxThenXxx
@Test
public void test_xxx_when_xxx_then_xxx() {
}
原则上统一就行。
覆盖率
Martin Fowler曾说过:将测试覆盖作为质量目标没有任何意义,应该把它作为一种发现未被测试覆盖的代码的手段。
覆盖率统计
- 接口覆盖率
- 行覆盖率
- 分支覆盖率
代码覆盖率的意义
- 分析未覆盖的代码,从而反推在前期测试设计是否充分,没有覆盖到的代码是否是测试设计的盲点,为什么没有考虑到?分析完原因之后,进行补充测试用例设计。
- 代码覆盖率高不能说明代码质量高,但反过来看,代码覆盖率低,代码质量不会高到哪里去,这可以作为测试自我审视的重要工具之一。
何时写测试
事实上,撰写测试代码的最好时机是在开始动手编码之前。这样可以帮助我们写出可测试性高的代码,一旦测试代码正常运行,意味着我们的开发工作差不多可以结束了。这有另一种叫法是测试驱动开发(TDD),测试驱动开发的编程方式依赖于下面这个短循环:先编写一个(失败的)测试,编写代码使测试通过,然后进行重构以保证代码整洁。
在重构代码之前,如果没有完备的测试代码,最好先补充测试代码再进行重构。我们需要确保重构前后的代码功能没有发生改变,这就需要测试代码进行校验。
测试覆盖哪些内容
- 观察被测试类应该做的所有事情,然后对这个类的每个行为进行测试,包括各种可能使它发生异常的边界条件。
- 对于太过简单的函数,不必要添加测试代码,因为不太可能出错。
- 先写一个正确的测试用例代码,观察被测试函数表现;再破坏条件,观察被测试函数是否出错。
- 探测边界条件。大多数人的测试都聚焦于正常的行为上(即"正常路径");同时,也需要考虑一些条件的边界处,这可以检查操作出错时软件的表现。如当我拿到一个集合时,我总想看看集合为空时会发生什么;如果拿到的是数值类型,0和负值会是不错的边界条件;
- 考虑可能出错的边界条件,把测试火力集中在那儿。
建议
- 确保所有测试都完全自动化,让它们检查自己的测试结果(断言)。
- 总是确保测试不该通过时真的会失败。
- 一套测试就是一个强大的bug侦测器,能够大大缩减查找bug的时间。
- 频繁地运行测试。对于你正在处理的代码,与其对应的测试至少每隔几分钟就要运行一次,每天至少运行一次所有的测试。
- 编写未臻完善的测试并经常运行,好过对完美测试的无尽等待。
- 每当你收到bug报告,请先写一个单元测试来暴露这个bug。
Part3 Mock
添加maven依赖
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.12.0</version>
<scope>test</scope>
</dependency>
创建mock对象
@InjectMocks
创建一个实例,简单来说这个Mock对象可以调用真实的方法,其余用@Mock或@Spy注解创建的mock对象将被注入到该实例中。
@InjectMocks
private UserController userController;
@Mock
创建一个虚假对象,调用该对象的函数均执行mock,不执行真正的部分。
// RETURNS_DEEP_STUBS表示对该对象依赖的成分也全部进行mock
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private UserService userService;
@Spy
对函数的调用执行真正的部分
@Spy
private UserService userService;
Mockito中的Mock和Spy都可用于拦截那些尚未实现或不期望被真正调用的对象和方法,并为其设置自定义行为。二者的区别在于,Mock不会真实调用,Spy会真实调用。
常见mock操作
mock普通方法
方式1:when(mock.call()).thenXxx().thenXxx();
// 示例
when(mock.someMethod("some arg"))
.thenThrow(new RuntimeException())
.thenReturn("foo");
方式2:doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod()
// 语法
doXxx().when(mock).call();
// 示例(mock异常方法)
doThrow(new RuntimeException()).when(mockedList).clear();
mock void方法
// 语法
doNothing().when(mock).callXxx();
// 示例
doNothing().doThrow(new RuntimeException())
.when(mock).someVoidMethod();
doNothing().when(spy).clear();
mock静态方法
业务代码
public class StaticUtils {
private StaticUtils() {}
public static List<Integer> range(int start, int end) {
return IntStream.range(start, end)
.boxed()
.collect(Collectors.toList());
}
public static String name() {
return "Baeldung";
}
}
无参静态方法
@Test
void givenStaticMethodWithNoArgs_whenMocked_thenReturnsMockSuccessfully() {
assertThat(StaticUtils.name()).isEqualTo("Baeldung");
try (MockedStatic<StaticUtils> utilities = Mockito.mockStatic(StaticUtils.class)) {
utilities.when(StaticUtils::name).thenReturn("Eugen");
assertThat(StaticUtils.name()).isEqualTo("Eugen");
}
assertThat(StaticUtils.name()).isEqualTo("Baeldung");
}
有参静态方法
@Test
void givenStaticMethodWithArgs_whenMocked_thenReturnsMockSuccessfully() {
try (MockedStatic<StaticUtils> utilities = Mockito.mockStatic(StaticUtils.class)) {
utilities.when(() -> StaticUtils.range(2, 6))
.thenReturn(Arrays.asList(10, 11, 12));
assertThat(StaticUtils.range(2, 6)).containsExactly(10, 11, 12);
}
assertThat(StaticUtils.range(2, 6)).containsExactly(2, 3, 4, 5);
}
// 格式
MockedStatic<T> utilities = Mockito.mockStatic(T.class)
utilities.when(() -> T.function(arg1, arg2)) .thenReturn(result);
mock私有方法
私有方法只能通过PowerMock进行mock,Mockito不支持。
mock构造函数
try (MockedConstruction mocked = mockConstruction(Foo.class)) {
Foo foo = new Foo();
when(foo.method()).thenReturn("bar");
assertEquals("bar", foo.method());
verify(foo).method();
}
assertEquals("foo", foo.method());
mock异常
// 方式1
doThrow(RuntimeException.class).when(mock).someVoidMethod();
// 方式2
when(mock.someMethod("some arg")).thenThrow(new RuntimeException());
Part4 断言
org.junit.jupiter.api.Assertions
常规校验
// 1.等值
// 元素相等
Assertions.assertEquals(T expected, T actual);
Assertions.assertNotEquals(T expected, T actual);
// 数组相等
Assertions.assertArrayEquals(T[] expected, T[] actual);
// 迭代器相等
Assertions.assertIterableEquals(Iterable<?> expected, Iterable<?> actual)
// 2.逐行比较
// 集合逐行比较
Assertions.assertLinesMatch(List<String> expectedLines, List<String> actualLines)
// 流逐行比较
Assertions.assertLinesMatch(Stream<String> expectedLines, Stream<String> actualLines)
// 3.真假
Assertions.assertTrue(condition);
// 4.是否为空
Assertions.assertNull(actual);
// 5.对象比较
// 对象相等
Assertions.assertSame(Object expected, Object actual)
// 对象不等
Assertions.assertNotSame(Object unexpected, Object actual)
// 6.执行次数
Mockito.verify(mock, Mockito.times(1)).call();
// 7.方法执行超时
Assertions.assertTimeout(Duration timeout, Executable executable)
// 8.校验实例类型
Assertions.assertInstanceOf(Class<T> expectedType, Object actualValue)
// 9.快速失败
Assertions.fail(xxx);
特殊校验
校验是否发生异常
方式1:Assertion.assertThrows()
@Test
public void test1() {
Assertions.assertThrows(NumberFormatException.class, () -> numExe());
}
private void numExe() {
Integer.parseInt("One");
}
方式2:try...catch捕获,然后在catch中对异常对象进行校验
public void test1() {
try {
numExe();
} catch(Exception ex) {
Assertions.assertInstanceOf(NumberFormatException.class, ex.getClass());
Assertions.assertEquals("xxx", ex.getMessage());
}
}
反射对象/方法校验
对于反射方法,抛出的异常会被捕获然后进行分装,最终对外呈现是InvocationTargetException,所以要拿到里层的异常进行结果断言校验。
Part5 参考
- PowerMock官方文档
- JUnit5
- Mockito3.5.10
- 《重构:改善既有代码的设计(第2版)》
- 《代码整洁之道》