在单元测试中,应尽量消除测试代码与系统、其他类之间的关系,在隔离的情况下测试代码。
Junit单元测试存在的问题
但是随着工程的壮大,使用上篇文章介绍的Junit进行单元测试,存在以下问题
-
待测试代码依赖于另个一类或接口
。如:
A-----------依赖------------->B-----------依赖------------->C
- 实际测试过程中,存在不容易构造的如·
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之后的对象,可以不必关心内部方法的调用关系,只需要给出方法的预期返回,即可直接使用。
- mock对象/接口:
- 不使用mock注解时,mock对象:
private LoginModel model;
model=mock(LoginModel.class)
- 使用mock注解,mock对象:
@Mock
private LoginModel model;
- 使用mock生成的类,所有方法都不是真实的方法,且方法返回值都是null。需要给出方法的预期返回。使用when就不会执行原来的方法。
when(LoginModel.login("01139949","123456")).thenReturn(true);
- 对void方法进行方法预期设定:
- void方法的模拟不支持when()
2. Spy
监视一个方法被调用时的情况,汇报调用过程中传递的参数。测试两个对象之间的协议或者关系时,较为方便。
- Spy对象:
- 不使用Spy注解时,spy对象:
private LoginModel model;
model=spy(LoginModel.class)
- 使用mock注解,mock对象:
@Spy
private LoginModel model;
- 所有@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进行单元测试的步骤为:
- 参数赋值,mock对象
- 写出方法期望值
- 获取方法实际值
- 断言/比较期望值、实际值:不关心方法的返回值,只关注方法是否被调用。
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));
}
}
}