Android单元测试(一):JUnit框架的使用

JUnit框架的使用

  • 前言
  • 工程介绍
  • JUnit介绍
    • 什么是JUnit
    • JUnit引入
    • JUnit注解
    • JUnit断言
    • JUnit使用
      • 基础用法
      • 参数化测试
      • assertThat用法
        • 用法
        • 自定义匹配器
      • @Rule用法
      • 套件测试

在开始之间先介绍一下单元测试框架之间的关系:
java单元测试框架:Junit、Mockito、Powermockito等;Android:Robolectric、AndroidJUnitRunner、Espresso等。
最开始建议先学习Junit & Mockito。junit运行在jvm上,所以只能测试纯java,若要测试依赖android库的代码,可以用mockito隔离依赖(下面会谈及)。
Junit官网
Mockito官网
之后学习AndroidJUnitRunner,Google官方的android单元测试框架之一,使用跟Junit是一样的,只不过需要运行在android真机或模拟器环境。由于mockito只在jvm环境生效,而android是运行在Dalvik或ART,所以AndroidJUnitRunner不能使用mockito。
然后可以尝试Robolectric & Espresso。Robolectric运行在jvm上,但是框架本身引入了android依赖库,所以可以做android单元测试,运行速度比运行在真机or模拟器快。但Robolectric也有局限性,例如不支持加载so,测试代码也有点别扭。当然,robolectric可以配合junit、mockito使用。Espresso也是Google官方的android单元测试框架之一,强大就不用说了,测试代码非常简洁。Espresso本身运行在真机上,因此android任何代码都能运行,不像junit&mockito那样隔离依赖。缺点也是显而易见,由于运行在真机,不能避免“慢”。
Robolectric官网

其实espresso应该是几款框架中最简单的,但笔者还是建议先学习junit&mockito。因为新手很可能会因为espresso的强大、简单,而忽略了junit做单元测试带来的巨大意义。例如,前文提到“快速定位bug”、“提高代码质量”,espresso慢,有违“快速”;用espresso不用修改工程任何代码,这不利于提高代码质量。

前言

以前工作中,没有写过测试用例,每次功能做完之后,都是运行在手机上或是模拟器上,进行测试。最近看了一些公司的招聘信息,部分公司要求会单元测试,于是了解了一下android单元测试的资料,我去,单元测试用起来真是好处多多,得抓紧时间学习一下了。

工程介绍

新建android项目,模板代码会默认在build文件中添加JUnit的依赖,而单元测试代码是放在src/test/java下面的,如下图
Android单元测试(一):JUnit框架的使用_第1张图片

JUnit介绍

什么是JUnit

Unit是Java编程语言的单元测试框架,用于编写和可重复运行的自动化测试。

JUnit引入

使用时在app的build文件中添加依赖(新建工程时,会默认在build文件中添加JUnit的依赖)

    testCompile 'junit:junit:4.12'

JUnit注解

注解 描述
@Test 测试注解,标记一个方法可以作为一个测试用例 。
@Before Before注解表示,该方法必须在类中的每个测试之前执行,以便执行某些必要的先决条件。
@BeforeClass BeforeClass注解指出这是附着在静态方法必须执行一次并在类的所有测试之前,这种情况一般用于测试计算、共享配制方法(如数据库连接)。
@After After注释表示,该方法在每项测试后执行(如执行每一个测试后重置某些变量,删除临时变量等)。
@AfterClass 当需要执行所有测试在JUnit测试用例类后执行,AlterClass注解可以使用以清理一些资源(如数据库连接),注意:方法必须为静态方法。
@Ignore 当想暂时禁用特定的测试执行可以使用这个注解,每个被注解为@Ignore的方法将不再执行
@Runwith @Runwith就是放在测试类名之前,用来确定这个类怎么运行的。也可以不标注,会使用默认运行器。
@Parameters 用于使用参数化功能。
@SuiteClasses 用于套件测试

执行顺序:@BeforeClass –> @Before –> @Test –> @After –> @AfterClass

JUnit断言

断言 描述
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使用

我们通过对下面的类进行简单类的测试,来说明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);
    }
}

基础用法

  • 首先测试方法stampToDate
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");
    }
}

测试步骤如下及测试结果展示如下图:
Android单元测试(一):JUnit框架的使用_第2张图片

  • 接下来测试 dateToStamp

我们知道在dateToStamp方法中,有抛出一个解析异常(ParseException)。也就是当参数没有按照规定格式去传,就会导致这个异常。Android单元测试(一):JUnit框架的使用_第3张图片
那我们怎么验证一个方法是否抛出了异常呢?可以给@Test注解设置expected参数来实现,如下:
Android单元测试(一):JUnit框架的使用_第4张图片
这样,如果抛出了异常,则测试用例通过。

参数化测试

按照以上方法进行测试,每进行一次测试都要修改参数及相应的值,使用以下方法可以避免这种麻烦。

首先在测试类上添加注解@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秒"
        });
    }
}

测试结果如下,一共有3个测试用例,只有1个测试用例通过。
Android单元测试(一):JUnit框架的使用_第5张图片

assertThat用法

用法

上面我们所用到的一些基本的断言,如果我们没有设置失败时的输出信息,那么在断言失败时只会抛出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进行测试,用例失败时所显示的具体错误信息。可以看到错误信息很详细!
Android单元测试(一):JUnit框架的使用_第6张图片

自定义匹配器

除了以上默认的匹配器,匹配器还可以自定义
自定义匹配器,需要继承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());
    }
}

正常号码测试结果:
Android单元测试(一):JUnit框架的使用_第7张图片
错误号码测试结果:
Android单元测试(一):JUnit框架的使用_第8张图片

@Rule用法

可以使用@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("执行测试用例!");
    }
}

测试结果如下:
Android单元测试(一):JUnit框架的使用_第9张图片
虽然使用@Before和@After也能做到,但是如果每次在测试之前和之后都要加入固定的内容,需要在每个测试类中都有@Before和@After注解的方法,这样比较麻烦。

套件测试

“套件测试”是指捆绑了几个单元测试用例并运行起来。在JUnit中,@RunWith 和 @Suite 这两个注解是用来运行套件测试。先来创建几个测试类

@RunWith(Suite.class)
@Suite.SuiteClasses({
        /**
         * 此处类的匹配顺序会影响执行顺序
         */
        ExampleUnitTest1.class,
        ExampleUnitTest2.class,
        ExampleUnitTest3.class,
        ExampleUnitTest4.class
})
public class JunitTestSuite {

}

测试用例的通过情况如下:
Android单元测试(一):JUnit框架的使用_第10张图片

你可能感兴趣的:(android,单元测试,JUnit)