单元测试详细设计与实现
单元测试原则:
- 自动通过一系列的断言给出执行结果,而不需要人为去判断(阿里开发手册中规定不允许有输出去肉眼判断)(A)
- 测试用例之间不能相互依赖影响,是独立的(I)
- 单元测试是可以重复执行的,不能受到外界环境的影响,如数据库、远程调用、中间件等外部依赖不能影响测试用例的执行(R)
基于以上原则,在单侧阶段,我们尽可能的不依赖Spring容器,而是对外部依赖进行mock,从而达到更加快速的测试。
框架选择
基于以上对比,我们可以选择阿里的TestableMock或者选择Mockito+Powermock。这里以第二种方案为例。
测试准备
命名规范:
在Mockito和Powermock中对命名没有特殊要求,但是我们最好在test目录下建立被测类在main目录下相同的路径,并以被测试类+Test命名。这在idea中有快捷键很好的生成。(快捷键 Ctrl + Shift + T 或者利用Generate中Test,能够快捷的在test下生成对应目录的Test类。)
依赖:
通常我们在spring项目中只要引入 spring-boot-starter-test 依赖就行,它包含了一些常用的模块 Junit、Spring Test、AssertJ、Hamcrest、Mockito 等。别的项目自行导入即可。
测试流程
写单测一般包括3个部分
1. Given(Mock外部依赖&准备Fake数据)
2. When(调用被测方法)
3. Then(断言执行结果)
案例分析
@Service
public class UserService {
@Autowired
private UserDao userDao;
private UserMsg userMsg;
public String saySomething() {
String user = userDao.getUserName();
String key = userMsg.getKey();
return user + key;
}
}
@RunWith(MockitoJUnitRunner)
public class UserServiceTest{
@InjectMocks
private UserService userService;
@Mock private UserDao userDao;
@Mock private UserMsg userMsg;
public void saySomething(){
//Given
when(userDao.getUserName()).thenReturn("小明");
when(userMsg.getKey()).thenReturn("爱学习");
//When
String message = userServic.saySomething()
//Then
assertNotNull(message);//断言不为空
assertEquals(message,"小明爱学习");//断言相等
verify(userDao).getUserName();//验证方法被执行了
verify(userMsg,times(1)).getKey();//验证方法执行次数
//验证执行顺序
InOrder inOrder = Mockito.inOrder(userDao,userMsg);
inOrder.verify(userDao).getUserName();
inOrder.verify(userServic).getKey();
}
}
注意事项
Mocktio的局限:
不能mock静态方法
不能mock private方法
不能mock final 方法
不能mock构造方法
解决方案:
PowerMock基于Mockito开发,起语法规则与Mockito基本一致
可以用PowerMock实现完成对private/static/final和构造方法的Mock。
PowerMock使用
相关注解:
@RunWith(PowerMockRunner.class) 注解表明使用PowerMockRunner运行测试用例
@PrepareForTest({NodeScheduler.class, NodeService.class,})解是用来添加所有需要测试的类,这里列举了两个需要测试的类。
添加依赖:
testImplementation(“org.powermock:powermock-api-mockito2:2.0.9”)
testImplementation(“org.powermock:powermock-module-junit4:2.0.9”)
Mock final
public class BookDao {
public final boolean stored(Book book) {
System.out.println("......confirm whether specified book is stored by BookDao......");
return true;
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({BookService.class, BookDao.class})
public class BookServiceTestWitPowerMock {
@Test
public void isStored() {
//mock final
Book book = PowerMockito.mock(Book.class);
BookDao bookDao = PowerMockito.mock(BookDao.class);
PowerMockito.when(bookDao.stored(book)).thenReturn(false);
}
}
Mock private
public class BookService {
private boolean checkExist(String name) {
System.out.println("---BookService checkExist---");
throw new UnsupportedOperationException("UserService checkExist unsupported exception.");
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest(BookService.class)
public class BookServiceTestWithPowerMock {
@Test
public void exist() throws Exception {
//mock private
BookService bookService = PowerMockito.spy(new BookService());
PowerMockito.doReturn(true).when(bookService, "checkExist", arg);
}
Mock static
public class BookDao {
public static void insert(Book book) {
throw new UnsupportedOperationException("BookDao does not support insert() operation.");
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({BookService.class, BookDao.class})
public class BookServiceTestWithPowerMock {
@Test
public void count() {
//Mock静态
PowerMockito.mockStatic(BookDao.class);
PowerMockito.when(BookDao.count()).thenReturn(10);
}
}
Mock New
public class UserService{
public void saveUser(String username, String password){
User user = new User(username,password);
user.insert();
}
@Test
public void saveUser() throw Exception{
String username = "user1";
String password = "aaa";
//mock new
User user = PowerMockito.mock(User.class);
PowerMockito.wenNew(User.class).withArguments(username,passsword).thenReturn(user);
.....
}
覆盖率
- Idea自带
覆盖率框架
JaCoCogradle中配置
执行 gradle test 就可以在 build\reports\jacoco 目录下找到报告了。
注意事项
JaCoCo与PowermMock一起使用时,会产生冲突,原因是JaCoCo会忽略注解@PrepareForTest({})里面的类,解决办法是用JaCoCo的离线模式。