Skip to main content

单元测试

从测试金字塔中我们可以看出,单元测试在整个金子塔中处于基石的地位。一般占有整体测试总量的70%,是测试的重中之重。

测试工具#

名称作用
Junit单元测试框架。
Mockito模拟依赖的库,主要用于在单元测试中提供模拟依赖。
PowerMock模拟依赖的库,此库可以模拟 Mockito 无法模拟的一些依赖,如:final、static、private 方法的模拟。

断言#

断言是判断程序的执行结果是否符合预期的工具,使用断言是单元测试的基础,要更好的进行单元测试的开发,首先要学会使用断言,为了大家更好的理解单元测试,这里将对Junit中的断言做一个简要的介绍。

Junit 断言#

  1. assertEquals / assertNotEquals
    验证是否相等,其方法原型如下:
assertEquals([String message],expected,actual,tolerance)
  • message:可选消息,提供此参数,将会在发生错误时报告这个消息。
  • expected:期望值。
  • actual:实际值。
  • tolerance:容差,如果比较的是两个浮点数,则表示在这个误差之内会被认为是相等的。

assertNotEqualsassertEquals用法相似,语义相反,不再赘述。

  1. assertTrue / assertFalse
    验证是否为真,其方法原型如下:
assertTrue([String message], Boolean condition)
  • message:可选消息,提供此参数,将会在发生错误时报告这个消息。
  • condition:待验证的布尔型值。

assertFalseassertTrue用法相似,语义相反,不再赘述。

  1. assertNull / assertNotNull
    验证是否为空(null),其方法原型如下:
assertNull([String message], Object object)
  • message:可选消息,提供此参数,将会在发生错误时报告这个消息。
  • object:待验证的对象。

assertNotNullassertNull用法相似,语义相反,不再赘述。

  1. assertSame / assertNotSame
    验证对象是否为相同,其方法原型如下:
assertSame([String message], Object expected, Object actual)
  • message:可选消息,提供此参数,将会在发生错误时报告这个消息。
  • expected:期望对象。
  • actual:实际对象。

assertNotSameassertSame用法相似,语义相反,不再赘述。

  1. assertArrayEquals
    验证数组是否为相等,注意这里比较的是数组中的元素,数组中所有元素相等的情况下,验证通过。其方法原型如下:
assertArrayEquals([String message], expecteds, actuals)
  • message:可选消息,提供此参数,将会在发生错误时报告这个消息。
  • expecteds:期望数组对象。
  • actuals:实际数组对象。
  1. fail
    执行fail方法会让验证直接失败。常用于测试异常。
fail([String message])

使用场景举例:

@Test
publicvoid passwordLengthLessThan6LettersThrowsException(){
try{
Password.validate("123");
fail("No exception thrown.");
}catch(Exception ex){
assertTrue(exinstanceofInvalidPasswordException);
assertTrue(ex.getMessage().contains("contains at least 6"));
}
}

以上的代码目标就是测试异常,所以当Password.validate("123")没有抛出异常时,测试失败,直接调用fail方法,本次测试失败。

  1. assertThat
    查看实际的值是否满足指定验证条件,满足验证条件的情况下,验证通过。
assertThat([String reason], T actual, Matcher<? super T> matcher)
  • reason:可选参数,提供此参数,将会在发生错误时报告这个信息。
  • actual:实际对象。
  • matcher:验证规则。

assertThatJunit4 以后加入的断言。这是一个功能十分强大的断言,因为后面的匹配器可以自己扩展,这就为 Junit 的断言系统提供了无限的可能。

Hamcrest#

Hamcrest是一个断言匹配器的库,使用Hamcrest可以极大的提高测试断言的可读性。除Java外,也可以在其他语言中应用 Hamcrest ,如:PythonRubyObjective-CPHPErlangSwift等。

Hamcrest在与Junit连用时,可将其视为Junit中的assertThat断言的扩展。这个扩展非常强大,可以说是吊打Junit原生断言的存在。

这里将根据Hamcrest中API的分类来对其进行一个简要的介绍。

核心API#

  1. is
    is可以看做是一个语法糖,使用is匹配器可以使程序更加易懂。当实际值与is中的条件可以正确匹配时,验证通过。
String str1 = "text";
String str2 = " text ";
assertThat(str1, is(equalToIgnoringWhiteSpace(str2)));
  1. not
    is使用方式一样,只是含义相反。当实际值与not中的条件不匹配时,验证通过。
String str1 = "text";
String str2 = " texts ";
assertThat(str1, not(equalToIgnoringWhiteSpace(str2)));
  1. containsString
    当被验证字符串是实际字符串的子串时,验证通过。
String str1 = "texts";
String str2 = "text";
assertThat(str1, containsString(str2));
  1. startsWith/endsWith
    当实际字符串以被验证字符串开头/结尾时,验证通过。
String str1 = "texts";
String str2 = "text";
assertThat(str1, startsWith(str2));
String str1 = "stext";
String str2 = "text";
assertThat(str1, endsWith(str2));
  1. sameInstance
    验证两个对象是否是相同实例。
Cat cat = new Cat();
assertThat(cat, sameInstance(cat));
  1. anyOf
    连接两个条件,当其中任意一个条件为真时验证通过,类似OR的逻辑。
String str = "calligraphy";
String start = "call";
String end = "foo";
assertThat(str, anyOf(startsWith(start), containsString(end)));
  1. allOf
    连接两个条件,当满足其中所有条件为真时验证通过,类似AND的逻辑。
String str = "calligraphy";
String start = "call";
String end = "phy";
assertThat(str, allOf(startsWith(start), endsWith(end)));
  1. hasItem
    检验集合类中是否存在指定元素,如果存在指定元素,则验证通过。
List<String> hamcrestMatchers = Arrays.asList("collections", "beans", "text", "number");
assertThat(hamcrestMatchers,hasItem("collections"));
Number 匹配器#
  1. greaterThan/lessThan
    比较数字大小的断言:
  • greaterThan:大于
  • greaterThanOrEqualTo:大于等于
  • lessThan:小于
  • lessThanOrEqualTo:小于等于
assertThat(2, greaterThan(1));
assertThat(1, greaterThanOrEqualTo(1));
assertThat(1, lessThan(2));
assertThat(1, lessThanOrEqualTo(1));
  1. closeTo
    误差基准与允许范围,使用方式参照示例。
//误差在1.0正负0.04内则验证通过
assertThat(1.03, is(closeTo(1.0, 0.04)));

Text 匹配器#

  1. isEmptyString/isEmptyOrNullString
  • isEmptyString:匹配空字符串。
  • isEmptyOrNullString:匹配空字符串或者null
String str = "";
assertThat(str, isEmptyString());
assertThat(str, isEmptyOrNullString());
  1. equalToIgnoringCase/equalToIgnoringWhiteSpace
  • equalToIgnoringCase:匹配空字符串相等,忽略大小写。
  • equalToIgnoringWhiteSpace:匹配空字符串相等,忽略整体左右空格。
String str1 = "TEXT";
String str2 = " text ";
assertThat(str1, equalToIgnoringCase(str2));
String str1 = "text";
String str2 = " text ";
assertThat(str1, equalToIgnoringWhiteSpace(str2));
  1. stringContainsInOrder 按顺序包含字符串。
String str = "calligraphy";
assertThat(str, stringContainsInOrder(Arrays.asList("call", "graph")));

Collections 匹配器#

  1. empty
    空集合判断。实际集合为空时,验证通过。
List<String> emptyList = new ArrayList<>();
assertThat(emptyList, empty());
  1. hasSize
    检查集合元素数量。
List<String> hamcrestMatchers = Arrays.asList("collections", "beans", "text", "number");
assertThat(hamcrestMatchers, hasSize(4));

Bean 匹配器#

  1. hasProperty
    检查Bean对象中是否存在某个属性。
City city = new City("shenzhen", "CA");
// city 中是否存在 state 属性
assertThat(city, hasProperty("state"));
// city 中是否存在 state 属性,且属性值为 "CA"
assertThat(city, hasProperty("state", equalTo("CA")));
...
public class City {
String name;
String state;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}

注意,hasProperty 验证的 Bean 必须是遵循 JavaBean 规范的对象,也就是说,如果上例中的 city 类的属性没有 gettersetter 方法,则验证无法通过。

  1. samePropertyValuesAs 检查两个对象中的属性值是否相等,如果相等则验证通过。
City city1 = new City("大连", "辽宁");
City city2 = new City("大连", "辽宁");
assertThat(city1, samePropertyValuesAs(city2));

异步任务#

在Android开发中不可避免地会涉及到许多的异步任务,异步任务不仅仅在Android开发领域,甚至在整个前端开发领域,都是一个绕不开且无法回避的重要话题。

那么在Android开发的过程中,如何对异步任务做单元测试呢?下面将给出两种基于 Mockito 的解决方案。

  1. Mockito ArgumentCaptor
    MockitoArgumentCaptor 对象可以在 verify 的时候通过 ArgumentCaptor.capture() 方法捕获传入的参数对象,这样我们就可以通过这个方法捕获 callback 对象,然后通过手动调用 callback 对象的回调方法来实现模拟异步回调操作。
    ArgumentCaptor 模拟异步步骤:
    - 首先,利用 `capture` 方法捕获传入的回调参数对象。
    - 然后,手动调用这个对象的回调方法,模拟各种情况。
    - 最后,验证回调方法中的逻辑。
@Test
public void login() {
final String[] token = { null };
HandleResponseHeaderRequestCallbackListener listener = new HandleResponseHeaderRequestCallbackListener() {
@Override
public void onHandleResponseHeaders(Headers headers) {
token[0] = headers.get("x-access-token");
}
};
mUserRepository.login("username", "password", listener);
// 首先,利用 `capture` 方法捕获真实对象。
Mockito.verify(authService).authorizations(Mockito.eq("username"), Mockito.eq("password"),
callbackArgumentCaptor.capture());
// 然后,手动调用这个对象的回调方法。
Headers headers = new Headers.Builder().add("x-access-token","accessToken").build();
callbackArgumentCaptor.getValue().onHandleResponseHeaders(headers);
// 最后,验证回调方法中的逻辑。
assertEquals(token[0], "accessToken");
}
  1. Mockito doAnswer
    doAnswer 方法可以模拟在某个方法被调用时,返回指定的结果。我们在构造返回结果的时候,可以通过 invocation 对象获取调用过程中传入的参数。我们利用 doAnswer 的这个特点模拟异步操作。
    doAnswer 模拟异步步骤:
    - 首先,利用 `doAnswer` 方法的 `invocation` 对象获取传入的回调参数对象。
    - 然后,手动调用这个对象的回调方法,模拟各种情况。
    - 最后,调用 `doAnswer` 中处理的方法,验证回调方法中的逻辑。
@Test
public void login() {
final String[] token = { null };
HandleResponseHeaderRequestCallbackListener listener = new HandleResponseHeaderRequestCallbackListener() {
@Override
public void onHandleResponseHeaders(Headers headers) {
token[0] = headers.get("x-access-token");
}
};
Mockito.doAnswer(invocation -> {
// 利用 `doAnswer` 方法的 `invocation` 对象获取传入的回调参数对象。
HandleResponseHeaderRequestCallbackListener handleResponseHeaderRequestCallbackListener = invocation.getArgument(4);
// 手动调用这个对象的回调方法,模拟各种情况。
Headers headers = new Headers.Builder().add("x-access-token", "accessToken").build();
handleResponseHeaderRequestCallbackListener.onHandleResponseHeaders(headers);
return null;
}).when(authService).authorizations(anyString(), anyString(), anyString(), anyString(), any(HandleResponseHeaderRequestCallbackListener.class));
// 调用 `doAnswer` 中处理的方法,验证回调方法中的逻辑。
authService.authorizations("super","super", "", "", listener);
assertEquals(token[0], "accessToken");
}

参考资料:
Android 单元测试第六篇(Hamcrest 匹配器)
Testing with Hamcrest
Java Hamcrest Unit testing asynchronous methods with Mockito
Testing Callbacks with Mockito