单元测试中使用Mockito模拟对象

单元测试应该小巧玲珑,轻盈快捷。然而,一个待测的对象可能依赖另一个对象。它可能需要跟数据库、邮箱服务器、Web Service、消息队列等服务进行交互。但是,这些服务可能在测试过程中不可用。假设这些服务可用,依赖这些服务的单元测试可能相当耗时。要是

  1. Web Service 不可获得。
  2. 数据库因维护而关闭。
  3. 消息队列笨重且缓慢。

这些违背单元测试小巧玲珑,轻盈快捷的初衷。单元测试被期待在几毫秒内执行完成。若单元测试缓慢,你的开发过程受阻,这会影响你开发组的效率。解决之道就是模拟(Mocking),

若你遵循OOP的SOILD原则,且使用Spring的依赖注入,单元测试中的模拟Mock变得轻而易举。你不必连接数据库。你只需一个能返回你期待值的对象。若你编写紧密耦合代码,模拟会相当艰难。我目睹过许多因紧密耦合其它对象的遗留代码不能单元测试。不可测试代码不遵循OOP的SOILD原则,且不能使用依赖注入。

Mockito初体验

接下来将学习使用Mockito框架。它是一套通过简单的方法对于指定接口或类生产Mock对象的类库。使用Mockito,在准备阶段只需少量时间,可以使用简洁的API编写漂亮的测试,可以对具体类创建Mock对象,并且有监视非Mock对象的功能。

这有两个术语需要了解一下。

  • Stub对象作用是在测试时提供所需的测试数据,可以对各种交互设置相应的回应。Mockito中when(...).thenReturn(...)这样的语法便是设置方法调用的返回值。另外也可以设置方法在何时调用会抛异常等。

  • Mock对象用来验证测试中所依赖对象间的交互是否能够达到预期。Mockito中用verify(...).methodXxx(...)语法验证methodXxx()方法是否按照预期进行调用。

需要加入到pom.xml的依赖如下:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.16.0</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

创建Mock对象

可以通过两种方法来Mock对象

  1. 通过mock(Class<T> clazz)方法。
  2. 通过@Mock注解需要Mock的对象,然后调用MockitoAnnotations.initMocks(this)@RunWith(MockitoJUnitRunner.class)初始化模拟。
//@RunWith(MockitoJUnitRunner.class)
public class MockitoSampleTest {
    
    
    // 模拟接口
    UserService mockUserService = mock(UserService.class);
    // 模拟实现类
    UserServiceImpl mockServiceImpl = mock(UserServiceImpl.class);
    // 基于注释模拟类
    @Mock
    User mockUser;

    @Before
    public void initMocks() {
    
    
	    // 初始化当前测试类所有@Mock注释模拟对象
   		MockitoAnnotations.initMocks(this);
    }
}

值得注意的是,对于final类、匿名类和Java的基本类型是无法进行Mock的。

设定Mock对象的期望值行为及返回值

有两种通用基础设定写法:

  1. when(...).thenReturn(...);
  2. doReturn(...).when(...).someMethod();

但是,doReturn(...).when(mockObj.someMethod());会抛异常。

@Test
public void testMockClass() {
    
    
    // 对方法设定返回值,也就是设置数据桩
    when(mockServiceImpl.findUserByUserName("tom")).thenReturn(new User("tom", "1234"));
    doReturn(true).when(mockServiceImpl).hasMatchUser("tom", "1234");

    User user = mockServiceImpl.findUserByUserName("tom");
    boolean isMatch = mockServiceImpl.hasMatchUser("tom", "1234");
    assertNotNull(user);
    assertEquals(user.getUserName(), "tom");
    assertEquals(isMatch, true);

}

也值得注意的是,static和final修饰的方法无法进行设定的。

验证交互行为

Mock对象一旦建立便会自动记录自己的交互行为,所以,可以有选择地对其交互行为进行验证。

@Test
// 模拟接口UserService测试
public void testMockInterface() {
    
    
	// 对方法设定返回值,也就是设置数据桩
	when(mockUserService.findUserByUserName("tom")).thenReturn(new User("tom", "1234"));
	doReturn(true).when(mockUserService).hasMatchUser("tom", "1234");
	// 对void方法进行方法预期设定
	User u = new User("John", "1234");
	doNothing().when(mockUserService).registerUser(u);

	// 执行方法调用
	User user = mockUserService.findUserByUserName("tom");
	boolean isMatch = mockUserService.hasMatchUser("tom", "1234");
	mockUserService.registerUser(u);

	assertNotNull(user);
	assertEquals(user.getUserName(), "tom");
	assertEquals(isMatch, true);

	// 验证交互行为
	verify(mockUserService).findUserByUserName("tom");
	// 验证方法只调用一次
	verify(mockUserService, times(1)).findUserByUserName("tom");
	// 验证方法至少调用一次
	verify(mockUserService, atLeastOnce()).findUserByUserName("tom");
	verify(mockUserService, atLeast(1)).findUserByUserName("tom");
	// 验证方法至多调用一次
	verify(mockUserService, atMost(1)).findUserByUserName("tom");

	verify(mockUserService).hasMatchUser("tom", "1234");
	verify(mockUserService).registerUser(u);
}

对Service层进行单元测试

同常主要Java Web应用分Controller,Service,DAO基本三层来进行开发。

接下来通过使用Mockito框架对Service进单元测试。

Domain领域对象:

public class Product {
    
    

}

DAO数据连接层:

public interface ProductDao {
    
    
	int getAvailableProducts(Product product);
	int orderProduct(Product product, int orderedQuantity);
}

Service业务逻辑层:

public class ProductService {
    
    
	private ProductDao productDao;

	public boolean buy(Product product, int orderedQuantity) {
    
    

		int availableQuantity = productDao.getAvailableProducts(product);
		if (orderedQuantity > availableQuantity) {
    
    
			return false;
		}
		
		productDao.orderProduct(product, orderedQuantity);
		
		return true;
	}

}

Service测试用例:

public class ProductServiceTest {
    
    
	
	private ProductDao productDao;

	@Before
	public void setupMock() {
    
    
		//模拟Dao层
		productDao = mock(ProductDao.class);
	}

	@Test
	public void testBuy() {
    
    
		int availableQuantity = 30;
		
		Product product = new Product();
		ProductService productService = new ProductService();
		
		//设置数据桩
		when(productDao.getAvailableProducts(product)).thenReturn(availableQuantity);
		//doReturn(availableQuantity).when(productDao).getAvailableProducts(product);
		
		//这写法不行
		//doReturn(availableQuantity).when(productDao.getAvailableProducts(product));
		
		//通过Spring测试框架提供的工具类为目标对象私有属性设值,这样就不用ProductDao另建setProductDao()方法
		ReflectionTestUtils.setField(productService, "productDao", productDao);
		
		Assert.assertFalse(productService.buy(product, 31));
		Assert.assertTrue(productService.buy(product, 3));

		//验证交互行为
		verify(productDao).orderProduct(product, 3);
		verify(productDao, times(2)).getAvailableProducts(product);

	}
	
}

测试用例中,用到Spring test框架的ReflectionTestUtils,它可以为目标对象非公有属性设值,或调用非公有setter方法,方便测试过程中使用。

为了使用ReflectionTestUtils,需要向pom.xml添加下面的依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>3.0.5.RELEASE</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>3.0.5.RELEASE</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
    <scope>test</scope>
</dependency>

总结

本文介绍了Mockito的基本用法,以及通过它Mock对象对Service层辅助测试用例。在Mockito辅助下,单元测试变得如虎添翼啊!

在编写代码过程中,必须反复调试它,保证他顺利通过。虽代码通过编译,只是说明其语法正确,但不能保证其语义也正确。没有任何人可以轻易承诺这段代码的行为一定是正确的。幸运的是,单元测试会为我们的承诺作出保证。编写单元测试就是用来验证这段代码的行为是否与我们期望的一样。有了单元测试,我们可以自信地交付自己的代码,减少后顾之忧。

引用

  1. Mocking in Unit Tests with Mockito

  2. Mockito (Mockito 2.16.0 API)

  3. 《Spring 3.x企业应用开发实战》陈雄华、林开雄 著

  4. Spring Framework Reference Documentation 11. Testing

猜你喜欢

转载自blog.csdn.net/u011863024/article/details/113706681