单元测试详细设计与实现

单元测试原则:

  1. 自动通过一系列的断言给出执行结果,而不需要人为去判断(阿里开发手册中规定不允许有输出去肉眼判断)(A)
  2. 测试用例之间不能相互依赖影响,是独立的(I)
  3. 单元测试是可以重复执行的,不能受到外界环境的影响,如数据库、远程调用、中间件等外部依赖不能影响测试用例的执行(R)

基于以上原则,在单侧阶段,我们尽可能的不依赖Spring容器,而是对外部依赖进行mock,从而达到更加快速的测试。

框架选择

gtest行覆盖率 preparefortest覆盖率为0_单元测试


基于以上对比,我们可以选择阿里的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);
	.....	
}

覆盖率

  1. Idea自带

覆盖率框架

gtest行覆盖率 preparefortest覆盖率为0_敏捷流程_02


JaCoCogradle中配置

gtest行覆盖率 preparefortest覆盖率为0_敏捷流程_03


gtest行覆盖率 preparefortest覆盖率为0_Test_04


执行 gradle test 就可以在 build\reports\jacoco 目录下找到报告了。

gtest行覆盖率 preparefortest覆盖率为0_gtest行覆盖率_05

注意事项
JaCoCo与PowermMock一起使用时,会产生冲突,原因是JaCoCo会忽略注解@PrepareForTest({})里面的类,解决办法是用JaCoCo的离线模式。