1.前言
网上有许多关于单元测试的好处,这里我就不去说了。我写单元测试的理由很简单粗暴,就是图一个方便。试想一下这个场景:我们在写一个新功能,每写一部分,我们就安装到手机上查看一下,这个过程中你要点击到对应的页面,做对应的操作,最后才能反馈给你结果。如果达到了预期效果,那么恭喜你。可是一旦这次失败了,是不是又要重复这一过程?是不是感到很麻烦?很费时间?如果你想早点写完下班,那么你就需要掌握单元测试。因为它能大大的缩短你自我验证的时间。
2.准备工作
我们新建一个项目,模板代码会默认在build文件中添加JUnit的依赖,而单元测试代码是放在src/test/java下面的,如下图:
用鼠标右键点击测试方法,选择菜单中的“Run”选项就可以执行对应的单元测试。我执行了图中的测试代码,可以看到执行方法只用了6毫秒,整个过程不到2秒。
3.JUnit介绍
JUnit是Java最基础的测试框架,主要的作用就是断言。
使用时在app的build文件中添加依赖。注意:用于测试环境框架一律是testCompile
开头。
dependencies { testCompile 'junit:junit:4.12' }
Assert类中主要方法如下:
方法名 | 方法描述 |
---|---|
assertEquals | 断言传入的预期值与实际值是相等的 |
assertNotEquals | 断言传入的预期值与实际值是不相等的 |
assertArrayEquals | 断言传入的预期数组与实际数组是相等的 |
assertNull | 断言传入的对象是为空 |
assertNotNull | 断言传入的对象是不为空 |
assertTrue | 断言条件为真 |
assertFalse | 断言条件为假 |
assertSame | 断言两个对象引用同一个对象,相当于“==” |
assertNotSame | 断言两个对象引用不同的对象,相当于“!=” |
assertThat | 断言实际值是否满足指定的条件 |
注意:上面的每一个方法,都有对应的重载方法,可以在前面加一个String类型的参数,表示如果断言失败时的提示。
JUnit 中的常用注解:
注解名 | 含义 |
---|---|
@Test | 表示此方法为测试方法 |
@Before | 在每个测试方法前执行,可做初始化操作 |
@After | 在每个测试方法后执行,可做释放资源操作 |
@Ignore | 忽略的测试方法 |
@BeforeClass | 在类中所有方法前运行。此注解修饰的方法必须是static void |
@AfterClass | 在类中最后运行。此注解修饰的方法必须是static void |
@RunWith | 指定该测试类使用某个运行器 |
@Parameters | 指定测试类的测试数据集合 |
@Rule | 重新制定测试类中方法的行为 |
@FixMethodOrder | 指定测试类中方法的执行顺序 |
执行顺序:@BeforeClass –> @Before –> @Test –> @After –> @AfterClass
4.JUnit用法
我们测试下面这个简单的时间转换工具类,来说明一下具体的用法。
public class DateUtil {
/** * 英文全称 如:2017-11-01 22:11:00 */
public static String FORMAT_YMDHMS = "yyyy-MM-dd HH:mm:ss";
/** * 掉此方法输入所要转换的时间输入例如("2017-11-01 22:11:00")返回时间戳 * * @param time * @return 时间戳 */
public static long dateToStamp(String time) throws ParseException{
SimpleDateFormat sdr = new SimpleDateFormat(FORMAT_YMDHMS, Locale.CHINA);
Date date = sdr.parse(time);
return date.getTime();
}
/** * 将时间戳转换为时间 */
public static String stampToDate(long lt){
String res;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(FORMAT_YMDHMS, Locale.CHINA);
Date date = new Date(lt);
res = simpleDateFormat.format(date);
return res;
}
}
1.基础用法
1.首先测试stampToDate
方法,测试时我认为返回的结果等于“预期时间”这个字符串。测试方法执行后如下图:
可以看到预期值与实际结果不符,测试失败!想要测试成功要么预期值为”2017-10-15 16:00:02”要么使用assertNotEquals
方法断言。
2.接下来测试dateToStamp
方法。
很简单,我认为返回结果不等于4,结果测试通过。
3.我们注意到在dateToStamp
方法中,有抛出一个解析异常(ParseException)。也就是当参数没有按照规定格式去传,就会导致这个异常。
那我们怎么验证一个方法是否抛出了异常呢?可以给@Test
注解设置expected
参数来实现,如下:
抛出了对应的异常则测试成功,反之则测试失败。
2.参数化测试
这时,你是不是觉得还是很麻烦,因为每次测试一个方法都要去设置对应的值,就不能连续用不不同的值去测试一个方法,省的我们不断地修改。这时就用到了@RunWith
与@Parameters
。
首先在测试类上添加注解@RunWith(Parameterized.class)
,在创建一个由 @Parameters
注解的public static方法,让返回一个对应的测试数据集合。最后创建构造方法,方法的参数顺序和类型与测试数据集合一一对应。
上图就是一个简单的例子,可以看到连续执行了三次测试,其中第二次测试没有抛出异常,测试失败!
3.assertThat用法
上面我们所用到的一些基本的断言,如果我们没有设置失败时的输出信息,那么在断言失败时只会抛出AssertionError
,无法知道到底是哪一部分出错。而assertThat
就帮我们解决了这一点。它的可读性更好。
assertThat(T actual, Matcher<? super T> matcher);
assertThat(String reason, T actual, Matcher<? super T> matcher);
其中reason
为断言失败时的输出信息,actual
为断言的值,matcher
为断言的匹配器。
常用的匹配器整理:
匹配器 | 说明 | 例子 |
---|---|---|
is | 断言参数等于后面给出的匹配表达式 | assertThat(5, is (5)); |
not | 断言参数不等于后面给出的匹配表达式 | assertThat(5, not(6)); |
equalTo | 断言参数相等 | assertThat(30, equalTo(30)); |
equalToIgnoringCase | 断言字符串相等忽略大小写 | assertThat(“Ab”, equalToIgnoringCase(“ab”)); |
containsString | 断言字符串包含某字符串 | assertThat(“abc”, containsString(“bc”)); |
startsWith | 断言字符串以某字符串开始 | assertThat(“abc”, startsWith(“a”)); |
endsWith | 断言字符串以某字符串结束 | assertThat(“abc”, endsWith(“c”)); |
nullValue | 断言参数的值为null | assertThat(null, nullValue()); |
notNullValue | 断言参数的值不为null | assertThat(“abc”, notNullValue()); |
greaterThan | 断言参数大于 | assertThat(4, greaterThan(3)); |
lessThan | 断言参数小于 | assertThat(4, lessThan(6)); |
greaterThanOrEqualTo | 断言参数大于等于 | assertThat(4, greaterThanOrEqualTo(3)); |
lessThanOrEqualTo | 断言参数小于等于 | assertThat(4, lessThanOrEqualTo(6)); |
closeTo | 断言浮点型数在某一范围内 | assertThat(4.0, closeTo(2.6, 4.3)); |
allOf | 断言符合所有条件,相当于&& | assertThat(4,allOf(greaterThan(3), lessThan(6))); |
anyOf | 断言符合某一条件,相当于或 | assertThat(4,anyOf(greaterThan(9), lessThan(6))); |
hasKey | 断言Map集合含有此键 | assertThat(map, hasKey(“key”)); |
hasValue | 断言Map集合含有此值 | assertThat(map, hasValue(value)); |
hasItem | 断言迭代对象含有此元素 | assertThat(list, hasItem(element)); |
下图为使用assertThat
测试失败时所显示的具体错误信息。可以看到错误信息很详细!
当然了匹配器也是可以自定义的。这里我自定义一个字符串是否是手机号码的匹配器来演示一下。
只需要继承BaseMatcher
抽象类,实现matches
与describeTo
方法。代码如下:
public class IsMobilePhoneMatcher extends BaseMatcher<String> {
/** * 进行断言判定,返回true则断言成功,否则断言失败 */
@Override
public boolean matches(Object item) {
if (item == null) {
return false;
}
Pattern pattern = Pattern.compile("(1|861)(3|5|7|8)\\d{9}$*");
Matcher matcher = pattern.matcher((String) item);
return matcher.find();
}
/** * 给期待断言成功的对象增加描述 */
@Override
public void describeTo(Description description) {
description.appendText("预计此字符串是手机号码!");
}
/** * 给断言失败的对象增加描述 */
@Override
public void describeMismatch(Object item, Description description) {
description.appendText(item.toString() + "不是手机号码!");
}
}
执行单元测试如下:
正确的手机号码测试成功:
错误号码测试失败:
5.@Rule用法
还记得一开始我们在@Before
与@After
注解的方法中加入”测试开始”的提示信息吗?假如我们一直需要这样的提示,那是不是需要每次在测试类中去实现它。这样就会比较麻烦。这时你就可以使用@Rule
来解决这个问题,它甚至比@Before
与@After
还要强大。
自定义@Rule
很简单,就是实现TestRule
接口,实现apply
方法。代码如下:
public class MyRule implements TestRule {
@Override
public Statement apply(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
// evaluate前执行方法相当于@Before
String methodName = description.getMethodName(); // 获取测试方法的名字
System.out.println(methodName + "测试开始!");
base.evaluate(); // 运行的测试方法
// evaluate后执行方法相当于@After
System.out.println(methodName + "测试结束!");
}
};
}
}
我们使用一下我们自定义的MyRule
,效果如图:
5.参考
PS:计划开始写有关Android单元测试的内容,因为涉及的测试框架比较多,所以由简至难开始,最终达到日常开发实用的阶段。(没想到这篇前后就用了一整天。。。)我也尽量快速的更新这一系列。代码已上传至Github。希望大家多多点赞支持!