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

Comments NOTHING