其实espresso应该是几款框架中最简单的,但笔者还是建议先学习junit&mockito。因为新手很可能会因为espresso的强大、简单,而忽略了junit做单元测试带来的巨大意义。例如,前文提到“快速定位bug”、“提高代码质量”,espresso慢,有违“快速”;用espresso不用修改工程任何代码,这不利于提高代码质量。
以前工作中,没有写过测试用例,每次功能做完之后,都是运行在手机上或是模拟器上,进行测试。最近看了一些公司的招聘信息,部分公司要求会单元测试,于是了解了一下android单元测试的资料,我去,单元测试用起来真是好处多多,得抓紧时间学习一下了。
新建android项目,模板代码会默认在build文件中添加JUnit的依赖,而单元测试代码是放在src/test/java下面的,如下图
Unit是Java编程语言的单元测试框架,用于编写和可重复运行的自动化测试。
使用时在app的build文件中添加依赖(新建工程时,会默认在build文件中添加JUnit的依赖)
testCompile 'junit:junit:4.12'
注解 | 描述 |
---|---|
@Test | 测试注解,标记一个方法可以作为一个测试用例 。 |
@Before | Before注解表示,该方法必须在类中的每个测试之前执行,以便执行某些必要的先决条件。 |
@BeforeClass | BeforeClass注解指出这是附着在静态方法必须执行一次并在类的所有测试之前,这种情况一般用于测试计算、共享配制方法(如数据库连接)。 |
@After | After注释表示,该方法在每项测试后执行(如执行每一个测试后重置某些变量,删除临时变量等)。 |
@AfterClass | 当需要执行所有测试在JUnit测试用例类后执行,AlterClass注解可以使用以清理一些资源(如数据库连接),注意:方法必须为静态方法。 |
@Ignore | 当想暂时禁用特定的测试执行可以使用这个注解,每个被注解为@Ignore的方法将不再执行 |
@Runwith | @Runwith就是放在测试类名之前,用来确定这个类怎么运行的。也可以不标注,会使用默认运行器。 |
@Parameters | 用于使用参数化功能。 |
@SuiteClasses | 用于套件测试 |
执行顺序:@BeforeClass –> @Before –> @Test –> @After –> @AfterClass
断言 | 描述 |
---|---|
void assertEquals([String message],expected value,actual value) | 断言两个值相等。值类型可能是int,short,long,byte,char,Object,第一个参数是一个可选字符串消息 |
void assertTrue([String message],boolean condition) | 断言一个条件为真 |
void assertFalse([String message],boolean condition) | 断言一个条件为假 |
void assertNull([String message],java.lang.Object object) | 断言一个对象为空(null) |
void assertNotNull([String message],java.lang.Object object) | 断言一个对象不为空(null) |
void assertSame([String message],java.lang.Object expected,java.lang.Object actual) | 断言两个对象引用相同的对象 |
void assertNotSame([String message],java.lang.Object unexpected,java.lang.Object actual) | 断言两个对象不是引用同一个对象 |
void assertArrayEquals([String message],expectedArray,resultArray) | 断言预期数组和结果数组相等,数组类型可能是int,short,long,byte,char,Object |
我们通过对下面的类进行简单类的测试,来说明JUnit是怎么使用的。
package com.app.wayee.androidunittest;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class DateUtil {
//日期转换
public static DateUtil INSTANCE = new DateUtil();
private SimpleDateFormat sdr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA);
/**
* 將時間格式如(2020-03-25 13:26:22)转化成时间戳
* @param time
* @return
* @throws ParseException
*/
public long dateToStamp(String time) throws ParseException {
return sdr.parse(time).getTime();
}
/**
* 将时间戳转化成时间
* @param stamp
* @return
*/
public String stampToDate(long stamp){
Date date = new Date(stamp);
return sdr.format(date);
}
}
public class ExampleUnitTest1 extends TestCase {
// private String time = "2020-03-25 15:00:00"; //1585119600000
private String time = "2020-03-25";
private long timeStamp = 4242511254555L;//2104-06-10 11:20:54
/**
* 测试将时间戳转成时间
*/
@Test
public void stampToDateTest(){
String timeResult = DateUtil.INSTANCE.stampToDate(timeStamp);
assertEquals(timeResult,"2104-06-10 11:20:54");
}
@Test
public void dateToStampTest() throws ParseException {
long data = DateUtil.INSTANCE.dateToStamp("2020-03-25");
assertEquals(data,"1585119600000");
}
}
我们知道在dateToStamp方法中,有抛出一个解析异常(ParseException)。也就是当参数没有按照规定格式去传,就会导致这个异常。
那我们怎么验证一个方法是否抛出了异常呢?可以给@Test注解设置expected参数来实现,如下:
这样,如果抛出了异常,则测试用例通过。
按照以上方法进行测试,每进行一次测试都要修改参数及相应的值,使用以下方法可以避免这种麻烦。
首先在测试类上添加注解@RunWith(Parameterized.class),在创建一个由 @Parameters 注解的public static方法,让返回一个对应的测试数据集合。最后创建构造方法,方法的参数顺序和类型与测试数据集合一一对应。
@RunWith(Parameterized.class)
public class ExampleUnitTest2 extends TestCase {
private String timeAuto;
public ExampleUnitTest2(String timeAuto){
this.timeAuto = timeAuto;
}
@Test
public void dateToStampWithParamTest() throws ParseException {
long data = DateUtil.INSTANCE.dateToStamp(timeAuto);
assertEquals(data,1585119600000L);
}
//该方法返回一个对应的测试数据集合,,,方法的参数顺序和类型与测试数据集合一一对应
@Parameterized.Parameters
public static Collection primeNumbers(){
return Arrays.asList(new String[]{
"2020-03-25",
"2020-03-25 15:00:00",
"2020年03月25日 16时00分02秒"
});
}
}
上面我们所用到的一些基本的断言,如果我们没有设置失败时的输出信息,那么在断言失败时只会抛出AssertionError,无法知道到底是哪一部分出错。而assertThat就帮我们解决了这一点。assertThat在失败的时候会抛出具体的失败信息,可读性更好。
assertThat(T actual, Matcher<? super T> matcher);
assertThat(String reason, T actual, Matcher<? super T> matcher);
其中reason为断言失败时的输出信息,actual为断言的值,matcher为断言的匹配器。
一般匹配器 | 说明 | 用例 |
---|---|---|
allOf | 匹配符表明如果接下来的所有条件必须都成立测试才通过,相当于“与”(&&) | assertThat( testedNumber, allOf( greaterThan(8), lessThan(16) ) ); |
anyOf | 匹配符表明如果接下来的所有条件只要有一个成立则测试通过,相当于“或”(||) | assertThat( testedNumber, anyOf( greaterThan(16), lessThan(8) ) ); |
anything | 匹配符表明无论什么条件,永远为true | assertThat( testedNumber, anything() ); |
is | 匹配符表明如果前面待测的object等于后面给出的object,则测试通过 | assertThat( testedString, is( “developerWorks” ) ); |
not | 匹配符和is匹配符正好相反,表明如果前面待测的object不等于后面给出的object,则测试通过 | assertThat( testedString, not( “developerWorks” ) ); |
containsString | 匹配符表明如果测试的字符串testedString 包含 子字符串"developerWorks"则测试通过 | assertThat( testedString, containsString( “developerWorks” ) ); |
endsWith | 匹配符表明如果测试的字符串testedString以子字符串"developerWorks"结尾则测试通过 | assertThat( testedString, endsWith( “developerWorks” ) ); |
startsWith | 匹配符表明如果测试的字符串testedString以子字符串"developerWorks"开始则测试通过 | assertThat( testedString, startsWith( “developerWorks” ) ); |
equalTo | 匹配符表明如果测试的testedValue等于expectedValue则测试通过,equalTo可以测试数值之间,字符串 | |
之间和对象之间是否相等,相当于Object的equals方法 | assertThat( testedValue, equalTo( expectedValue ) ); | |
equalToIgnoringCase | 匹配符表明如果测试的字符串testedString在忽略大小写的情况下等于"developerWorks"则测试通过 | assertThat( testedString, equalToIgnoringCase( “developerWorks” ) ); |
equalToIgnoringWhiteSpace | 匹配符表明如果测试的字符串testedString在忽略头尾的任意个空格的情况下等于"developerWorks"则 测试通过,注意:字符串中的空格不能被忽略 | assertThat( testedString, equalToIgnoringWhiteSpace( “developerWorks” ) ); |
closeTo | 匹配符表明如果所测试的浮点型数testedDouble在20.0±0.5范围之内则测试通过 | assertThat( testedDouble, closeTo( 20.0, 0.5 ) ); |
greaterThan | 匹配符表明如果所测试的数值testedNumber大于16.0则测试通过 | assertThat( testedNumber, greaterThan(16.0) ); |
lessThan | 匹配符表明如果所测试的数值testedNumber小于16.0则测试通过 | assertThat( testedNumber, lessThan (16.0) ); |
greaterThanOrEqualTo | 匹配符表明如果所测试的数值testedNumber大于等于16.0则测试通过 | assertThat( testedNumber, greaterThanOrEqualTo (16.0) ); |
lessThanOrEqualTo | 匹配符表明如果所测试的数值testedNumber小于等于16.0则测试通过 | assertThat( testedNumber, lessThanOrEqualTo (16.0) ); |
hasEntry | 匹配符表明如果测试的Map对象mapObject含有一个键值为"key"对应元素值为"value"的Entry项则测试通过 | assertThat( mapObject, hasEntry( “key”, “value” ) ); |
hasItem | 匹配符表明如果测试的迭代对象iterableObject含有元素“element”项则测试通过 | assertThat( iterableObject, hasItem ( “element” ) ); |
hasKey | 匹配符表明如果测试的Map对象mapObject含有键值“key”则测试通过 | assertThat( mapObject, hasKey ( “key” ) ); |
hasValue | 匹配符表明如果测试的Map对象mapObject含有元素值“value”则测试通过 | assertThat( mapObject, hasValue ( “key” ) ); |
以下是使用assertThat进行测试,用例失败时所显示的具体错误信息。可以看到错误信息很详细!
除了以上默认的匹配器,匹配器还可以自定义
自定义匹配器,需要继承BaseMatcher类,然后实现matches方法
public class IsMobilePhoneMatcher extends BaseMatcher<String> {
//进行断言判断,返回true则断言成功,否则断言失败
@Override
public boolean matches(Object item) {
if(item == null){
return false;
}
Pattern pattern = Pattern.compile("^1(3|4|5|7|8)\\d{9}$");
Matcher matcher = pattern.matcher((String)item);
return matcher.find();
}
/**
* 给期待断言成功的对象增加描述信息
* @param description
*/
@Override
public void describeTo(Description description) {
description.appendText("预计此字符串是手机号码");
}
/**
* 给断言失败的对象增加描述
* @param item
* @param description
*/
@Override
public void describeMismatch(Object item, Description description) {
super.describeMismatch(item, description);
description.appendText(item.toString()+" 不是手机号码");
}
}
使用自定义的匹配器
public class ExampleUnitTest3 {
@Test
public void testAssertThat3(){
assertThat("15128614731",new IsMobilePhoneMatcher());
}
@Test
public void testAssertThat4(){
assertThat("18000000000",new IsMobilePhoneMatcher());
}
}
可以使用@Rule方法在需要测试的方法之前和之后增加一些操作。Rule的定义方式如下,继承TestRule后,实现apply方法即可。
public class MyTestRule implements TestRule {
@Override
public Statement apply(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
//evaluate前執行方法相當於@Befor
String methodName = description.getMethodName(); // 获取测试方法的名字
System.out.println(methodName + "测试开始!");
base.evaluate(); // 运行的测试方法
// evaluate后执行方法相当于@After
System.out.println(methodName + "测试结束!");
}
};
}
}
Rule的使用:
public class ExampleUnitTest4 {
@Before
public void setUp(){
System.out.println("测试开始 @Before");
}
@After
public void tearDown(){
System.out.println("测试结束!@After");
}
@Rule
public MyTestRule rule = new MyTestRule();
@Test
public void testRule(){
System.out.println("执行测试用例!");
}
}
测试结果如下:
虽然使用@Before和@After也能做到,但是如果每次在测试之前和之后都要加入固定的内容,需要在每个测试类中都有@Before和@After注解的方法,这样比较麻烦。
“套件测试”是指捆绑了几个单元测试用例并运行起来。在JUnit中,@RunWith 和 @Suite 这两个注解是用来运行套件测试。先来创建几个测试类
@RunWith(Suite.class)
@Suite.SuiteClasses({
/**
* 此处类的匹配顺序会影响执行顺序
*/
ExampleUnitTest1.class,
ExampleUnitTest2.class,
ExampleUnitTest3.class,
ExampleUnitTest4.class
})
public class JunitTestSuite {
}