单元测试之Mockito

时间:2021-08-13 16:21:27

在单元测试中,应尽量消除测试代码与系统、其他类之间的关系,在隔离的情况下测试代码。

Junit单元测试存在的问题

但是随着工程的壮大,使用上篇文章介绍的Junit进行单元测试,存在以下问题

  1. 待测试代码依赖于另个一类或接口。如:

A-----------依赖------------->B-----------依赖------------->C

  1. 实际测试过程中,存在不容易构造的如·HttpServletRequest 或不容易获取的ResultSet的对象。

Mockito

Mockito-Java中最流行的mock测试框架。 
使用mock对象(模拟对象)模拟测试中的依赖,方便测试。一般使用Junit+Mockito的方式进行单元测试。

A-----------依赖------------->mock(B)

在Gradle中,声明mockito依赖
 testCompile 'org.mockito:mockito-all:1.10.19'

Mockito的基本注解

1. Mock

模拟外部依赖,提供测试所需的测试数据。mock之后的对象,可以不必关心内部方法的调用关系,只需要给出方法的预期返回,即可直接使用。

  1. mock对象/接口: 
    • 不使用mock注解时,mock对象: 
      private LoginModel model; 
      model=mock(LoginModel.class)
    • 使用mock注解,mock对象: 
      @Mock 
      private LoginModel model;
  2. 使用mock生成的类,所有方法都不是真实的方法,且方法返回值都是null。需要给出方法的预期返回。使用when就不会执行原来的方法。 
    when(LoginModel.login("01139949","123456")).thenReturn(true);
  3. 对void方法进行方法预期设定: 
    • void方法的模拟不支持when()

2. Spy

监视一个方法被调用时的情况,汇报调用过程中传递的参数。测试两个对象之间的协议或者关系时,较为方便。

  1. Spy对象: 
    • 不使用Spy注解时,spy对象: 
      private LoginModel model; 
      model=spy(LoginModel.class)
    • 使用mock注解,mock对象: 
      @Spy 
      private LoginModel model;
  2. 所有@Spy生成的类,所有方法都是真实的方法,且方法的返回值与真实方法一样。 
    • 模拟@Spy类的方法: 
      doReturn(true).when(model).login("01139949","123456");

3. Stub

人为设定函数的方法的替代,特定输入下的表现。当测试用例需要一个对象提供数据,可以使用Stub取代数据源,设置每次返回一致的模拟数据。 
when(mock.someMethod()).thenReturn(value)设置调用的方法的返回值, 
例子:

when(DeviceUtils.getDeviceId(any(Context.class))).thenReturn("deviceId");

Mockito支持迭代风格的返回值: 
when(mock.someMethod()).thenReturn(value).thenReturn(value);

例子:

LinkedList list = mock(LinkedList.class);
when(list .get(0)).thenReturn("first");
when(list .get(1)).thenThrow(new RuntimeException());

Mock与Stub的区别: 
Mock更关注方法实现的结果是都符合预期,是对象功能层面的概念; 
Stub更关注对象的某种或者某几种实现,是实现层面的概念。


对void方法进行方法预期设定 
void方法的模拟不支持when(mock.someMethod()).thenReturn(value)这样的语法,只支持下面的方式: 
doNothing()模拟不做任何返回(mock对象void方法的默认返回)

doNothing().when(i).remove(); 

doThrow(Throwable) 模拟返回异常

doThrow(new RuntimeException()).when(i).remove(); 

例子:

OutputStream mockStream = mock(OutputStream.class);
doThrow(new IOException()).when(mockStream).close();

4. Captor

需要验证一系列交互后传入方法的参数,需要用参数捕获器-ArgumentCaptor验证捕获的传入方法的参数,是否符合要求. 
通过ArgumentCaptor对象的forClass(Class clazz)方法来构建ArgumentCaptor对象, 捕获方法的参数,验证参数值。如果方法有多个参数需要验证,就要创建多个ArgumentCaptor对象捕获参数。

ArgumentCaptor的Api 
argument.capture(): 捕获方法参数 
argument.getValue(): 获取方法参数值,如果方法进行了多次调用,它将返回最后一个参数值 
argument.getAllValues():多次调用方法后,返回多个参数值

 ArgumentCaptor<BaseHttpRequestListener> captor = ArgumentCaptor.forClass(BaseHttpRequestListener.class);
RequestUtils.doHttpRequest(anyString(), anyMap(), captor.capture());
captor.getValue().onSuccess("");

5. InjectMocks

InjectMocks主要关注mock对象的行为交互,验证mock对象的方法调用参数、次数、顺序等,可实现自动注入mock对象.

InjectMocks 的问题:如果被测试类是代理类,注入会失败。注入失败的时候,Mockito不会抛出任何异常,需要自己手动验证安全性。

@InjectMocks 
private ArticleManager manager = new ArticleManager()
;

@Test
public void shouldDoSomething()
{
manager.initiateArticle();
verify(database).addListener(any(ArticleListener.class));
}

使用mockito进行单元测试的步骤为:

  1. 参数赋值,mock对象
  2. 写出方法期望值
  3. 获取方法实际值
  4. 断言/比较期望值、实际值:不关心方法的返回值,只关注方法是否被调用。 verify(mock).someMethod()

待测试类:LoginModel


public class LoginModel {

public LoginModel(ILoginBusiness iBusiness) {
super(iBusiness);
}
public void login(final String userName, final String password){
Map<String, String> params = new HashMap<>();
params.put("username", userName);
params.put("password", password);

RequestUtils.doHttpRequest(UrlConstants.LOGIN, params, new BaseHttpRequestListener(this) {
@Override
public void onSuccess(String result)
{
//
}
}
}
}

测试类:

public class LoginModelTest{
@RunWith(PowerMockRunner.class)
@PrepareForTest({RequestUtils.class})
public class LoginModelTest {
private LoginBusiness business;
private LoginModel model;

@Before
public void setUp() throws Exception {
business = mock(LoginBusiness.class);
model = new LoginModel(business);
PowerMockito.mockStatic(RequestUtils.class);
}

@Test(expected = NullPointerException.class)
public void should_throw_NullPointException_when_login_and_return_empty() throws Exception {
//given

//when
model.login("01139949", "123456");
//then
PowerMockito.verifyStatic();
ArgumentCaptor<BaseHttpRequestListener> captor = ArgumentCaptor.forClass(BaseHttpRequestListener.class);
RequestUtils.doHttpRequest(anyString(), anyMap(), captor.capture());
captor.getValue().onSuccess("");
}

@Test
public void should_backWarehouseSuccess_when_login() throws Exception {
//given

//when
model.login("000212", "000213");
//then
PowerMockito.verifyStatic();
ArgumentCaptor<BaseHttpRequestListener> captor = ArgumentCaptor.forClass(BaseHttpRequestListener.class);
RequestUtils.doHttpRequest(anyString(), anyMap(), captor.capture());
captor.getValue().onSuccess(new Gson().toJson(new UserResponse()));
verify(business).loginSuccess(eq("000212"), eq("000213"), any(UserResponse.class));
}
}
}