01-单元测试

nobility 发布于 7 天前 05-测试框架 23 次阅读


单元测试

随着开发越来越标准化,公司开始逐步要求开发者写单元测试了,不过,也只要求了对增量代码编写单元测试,如果我们只修改了一两行代码,却要写成百上千行的单元测试,就有些得不偿失了,所以我就利用了一些技巧,尽可能的缩小单元测试的范围,减少大家的工作量。

Junit 和 Mockito 的基本用法

@ExtendWith(MockitoExtension.class)
// 启用 Mockito 相关注解,否则 @Mock 等注解可能不生效,从而导致 NPE
public class UserServiceTest {
    @InjectMocks
    // 被检测对象注入
    private UserService userService;

    @Mock
    // 被检测对象所依赖的对象注入
    private UserRepo userRepo;

    @Test
    void getUserNameTest() {
        // mock 被检测对象方法中依赖的方法:传入任意参数都返回一个 User(1L, "Alice") 对象
        Mockito.when(userRepo.findById(ArgumentMatchers.any())).thenReturn(new User(1L, "Alice"));

        // 调用被检测对象的方法
        String name = userService.getUserName(1L);

        // 断言返回值
        assertEquals("Alice", name);
    }
}

Mock 私有方法

为了 Mock 部分不太好直接运行的私有方法,我们可以利用 PowerMockRunner 提供的扩展能力,来 Mock 私有方法的结果。

@RunWith(PowerMockRunner.class)
// 指定 JUnit 测试运行器为 PowerMock 的运行器,PowerMock 可以在测试执行前对字节码进行操作(Mock 私有方法、静态方法或构造方法),而标准的 JUnit 运行器或 Mockito 运行器不具备这种字节码操作能力,所以必须使用 PowerMockRunner 来启动测试
@PrepareForTest({UserServiceTest.class})
// 指定 PowerMock 在测试执行前需要“准备”哪些类,PowerMock 需要对指定的类进行字节码修改,才能 Mock 它们的静态方法、私有方法或构造方法,所以如果要 Mock 某个类的静态方法,那么就必须将该类放入 @PrepareForTest 中
public class UserServiceTest {
    @InjectMocks
    // 被检测对象注入
    private UserService userService;

    @Mock
    // 被检测对象所依赖的对象注入
    private UserRepo userRepo;

    @Test
    public void getUserNameTest() {
        // mock 被检测对象方法中依赖的方法:传入任意参数都返回一个 User(1L, "Alice") 对象
        PowerMockito.when(userRepo.findById(ArgumentMatchers.any())).thenReturn(new User(1L, "Alice"));

        // 将被检测对象包装成一个"间谍对象"
        UserService spy = PowerMockito.spy(userService);
        // Mock 间谍对象的私有方法
        PowerMockito
            // 返回值
            .doReturn(true)
            // 私有方法名 + 参数
            .when(spy, "isPrivate", any());

        // 调用间谍对象的方法
        String name = spy.getUserName(1L);

        // 断言返回值
        assertEquals("Alice", name);
    }
}

Mock 静态方法

为了 Mock 部分不太好直接运行的静态方法,我们可以利用 PowerMockRunner 提供的扩展能力,来 Mock 静态方法的结果。

@RunWith(PowerMockRunner.class)
// 指定 JUnit 测试运行器为 PowerMock 的运行器,PowerMock 可以在测试执行前对字节码进行操作(Mock 私有方法、静态方法或构造方法),而标准的 JUnit 运行器或 Mockito 运行器不具备这种字节码操作能力,所以必须使用 PowerMockRunner 来启动测试
@PrepareForTest({UserServiceTest.class, EnvConfig.class})
// 指定 PowerMock 在测试执行前需要“准备”哪些类,PowerMock 需要对指定的类进行字节码修改,才能 Mock 它们的静态方法、私有方法或构造方法,所以如果要 Mock 某个类的静态方法,那么就必须将该类放入 @PrepareForTest 中
public class UserServiceTest {
    @InjectMocks
    // 被检测对象注入
    private UserService userService;

    @Mock
    // 被检测对象所依赖的对象注入
    private UserRepo userRepo;

    @Test
    public void getUserNameTest() {
        // mock 被检测对象方法中依赖的方法:传入任意参数都返回一个 User(1L, "Alice") 对象
        PowerMockito.when(userRepo.findById(ArgumentMatchers.any())).thenReturn(new User(1L, "Alice"));

        // 指定将要 mock 的静态方法所属类
        PowerMockito.mockStatic(EnvConfig.class);
        // mock 静态方法
        PowerMockito.when(EnvConfig.isPre()).thenReturn(true);

        // 调用被检测对象的方法
        String name = userService.getUserName(1L);

        // 断言返回值
        assertEquals("Alice", name);
    }
}

调用私有方法

有时候我们只改了一个私有方法中的一行,不想从整个方法入口开始编写单元测试,所以我们可以利用 ReflectionTestUtils 来直接运行私有方法。

@ExtendWith(MockitoExtension.class)
// 启用 Mockito 相关注解,否则 @Mock 等注解可能不生效,从而导致 NPE
public class UserServiceTest {
    @InjectMocks
    // 被检测对象注入
    private UserService userService;

    @Test
    void getUserNameTest() {
        // 使用 ReflectionTestUtils 调用 userService 的私有方法 getUserName,入参是 1L,返回值类型是 Object,强转为 String
        String name = (String)ReflectionTestUtils.invokeMethod(userService, "getUserName", 1L);

        // 断言返回值
        assertEquals("Alice", name);
    }
}

内部存在异步线程的方法

部分业务方法为了提高性能可能会引入多线程并发处理(比如 CompletableFuture),但是在单元测试中,为了保证每次单元测试结果的一致性,所以可以将异步操作转换为同步执行,避免并发导致的结果不一致。

@ExtendWith(MockitoExtension.class)
// 启用 Mockito 相关注解,否则 @Mock 等注解可能不生效,从而导致 NPE
public class UserServiceTest {
    @InjectMocks
    // 被检测对象注入
    private UserService userService;

    @Mock
    // mock 一个线程池
    private ThreadPoolTaskExecutor executor;

    @Test
    void getUserNameTest() {
        // 拦截提交线程池 execute(Runnable runnable) 方法
        Mockito.doAnswer((InvocationOnMock invocationOnMock) -> {
            // 拦截传入 execute() 方法的第一个参数(即 Runnable 任务)
            Runnable runnable = (Runnable)invocationOnMock.getArguments()[0];
            // 立即在当前线程中执行这个异步任务
            runnable.run();
            // Runnable 无返回值,返回 null
            return null;
        }).when(executor).execute(any(Runnable.class));

        // 拦截提交线程池 submit(Callable callable) 方法
        Mockito.doAnswer((InvocationOnMock invocationOnMock) -> {
            // 拦截传入 submit() 方法的第一个参数(即 Callable 任务)
            Callable<?> callable = (Callable<?>)invocationOnMock.getArguments()[0];
            // 立即在当前线程中执行这个异步任务,并返回执行结果
            return callable.call();
        }).when(executor).submit(any(Callable.class));

        // mock 被检测对象方法中依赖的方法:传入任意参数都返回一个 User(1L, "Alice") 对象
        Mockito.when(userRepo.findById(ArgumentMatchers.any())).thenReturn(new User(1L, "Alice"));

        // 调用被检测对象的方法
        String name = userService.getUserName(1L);

        // 断言返回值
        assertEquals("Alice", name);
    }
}
加油啊!即便没有转生到异世界,也要拿出真本事!!!\(`Δ’)/
最后更新于 2026-04-08