UT的正确打开方式

238 阅读6分钟

Part1 前言

UT的作用:

  1. 帮助发现bug
  2. 放心重构
  3. 边界条件

通过此篇文章,可以学到如下内容:

  1. UT规范
  2. Mock
  3. 断言

Part2 开发者测试

FIRST原则

F:Fast,测试执行要快

如单条测试用例执行时间<1s;单个测试文件执行<1min

I:Independent,独立

  1. 每个测试用例都可以单独执行,不依赖其他任何用例,用例执行结果与顺序无关
  2. 测试用例之间不能相互调用
  3. 用例所需的初始化条件/数据在本用例中预置,不依赖用例的执行顺序
  4. 用例不能修改全局变量/状态,防止用例间的全局变量相互影响,导致用例随机失败

R:Repeatable,可重复

测试程序在不同环境可重复运行,且每次测试结果相同

S:Self-Validating,自校验

测试结果应该明确,无须人工检查来判断结果是否正确。(断言)

T:Timely,及时

单元测试代码和生产代码同步输出,及时验证,及时进行review,不要后续为了覆盖率而补用例。(TDD)

规范

目录结构

// UT代码目录
src/test/java

// UT资源文件目录
src/test/resources

用例命名

  1. 下划线方式:test_xxx_when_xxx_then_xxx
  2. 驼峰方式:testXxxWhenXxxThenXxx
@Test
public void test_xxx_when_xxx_then_xxx() {
}

原则上统一就行。

覆盖率

Martin Fowler曾说过:将测试覆盖作为质量目标没有任何意义,应该把它作为一种发现未被测试覆盖的代码的手段

覆盖率统计
  1. 接口覆盖率
  2. 行覆盖率
  3. 分支覆盖率
代码覆盖率的意义
  1. 分析未覆盖的代码,从而反推在前期测试设计是否充分,没有覆盖到的代码是否是测试设计的盲点,为什么没有考虑到?分析完原因之后,进行补充测试用例设计。
  2. 代码覆盖率高不能说明代码质量高,但反过来看,代码覆盖率低,代码质量不会高到哪里去,这可以作为测试自我审视的重要工具之一。

何时写测试

事实上,撰写测试代码的最好时机是在开始动手编码之前。这样可以帮助我们写出可测试性高的代码,一旦测试代码正常运行,意味着我们的开发工作差不多可以结束了。这有另一种叫法是测试驱动开发(TDD),测试驱动开发的编程方式依赖于下面这个短循环:先编写一个(失败的)测试,编写代码使测试通过,然后进行重构以保证代码整洁。

在重构代码之前,如果没有完备的测试代码,最好先补充测试代码再进行重构。我们需要确保重构前后的代码功能没有发生改变,这就需要测试代码进行校验。

测试覆盖哪些内容

  1. 观察被测试类应该做的所有事情,然后对这个类的每个行为进行测试,包括各种可能使它发生异常的边界条件。
  2. 对于太过简单的函数,不必要添加测试代码,因为不太可能出错。
  3. 先写一个正确的测试用例代码,观察被测试函数表现;再破坏条件,观察被测试函数是否出错。
  4. 探测边界条件。大多数人的测试都聚焦于正常的行为上(即"正常路径");同时,也需要考虑一些条件的边界处,这可以检查操作出错时软件的表现。如当我拿到一个集合时,我总想看看集合为空时会发生什么;如果拿到的是数值类型,0和负值会是不错的边界条件;
  5. 考虑可能出错的边界条件,把测试火力集中在那儿。

建议

  1. 确保所有测试都完全自动化,让它们检查自己的测试结果(断言)。
  2. 总是确保测试不该通过时真的会失败。
  3. 一套测试就是一个强大的bug侦测器,能够大大缩减查找bug的时间。
  4. 频繁地运行测试。对于你正在处理的代码,与其对应的测试至少每隔几分钟就要运行一次,每天至少运行一次所有的测试。
  5. 编写未臻完善的测试并经常运行,好过对完美测试的无尽等待。
  6. 每当你收到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(26))
          .thenReturn(Arrays.asList(101112));

        assertThat(StaticUtils.range(26)).containsExactly(101112);
    }
    assertThat(StaticUtils.range(26)).containsExactly(2345);
}

// 格式
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 参考

  1. PowerMock官方文档
  2. JUnit5
  3. Mockito3.5.10
  4. 《重构:改善既有代码的设计(第2版)》
  5. 《代码整洁之道》