Android單元測(cè)試(五):JUnit進(jìn)階

前面一章介紹了JUnit的一些基本用法,本章來(lái)介紹關(guān)于JUnit更高級(jí)的用法,這些功能我們可能并不一定會(huì)用到,但是了解它,對(duì)JUnit會(huì)有更深刻的認(rèn)識(shí)。

5.1 Test runners

大家剛開(kāi)始使用JUnit的時(shí)候,可能會(huì)跟我一樣有一個(gè)疑問(wèn),JUnit沒(méi)有main()方法,那它是怎么開(kāi)始執(zhí)行的呢?眾所周知,不管是什么程序,都必須有一個(gè)程序執(zhí)行入口,而這個(gè)入口通常是main()方法。顯然,JUnit能直接執(zhí)行某個(gè)測(cè)試方法,那么它肯定會(huì)有一個(gè)程序執(zhí)行入口。沒(méi)錯(cuò),其實(shí)在org.junit.runner包下,有個(gè)JUnitCore.java類(lèi),這個(gè)類(lèi)有一個(gè)標(biāo)準(zhǔn)的main()方法,這個(gè)其實(shí)就是JUnit程序的執(zhí)行入口,其代碼如下:

public static void main(String... args) {
    Result result = new JUnitCore().runMain(new RealSystem(), args);
    System.exit(result.wasSuccessful() ? 0 : 1);
}

通過(guò)分析里面的runMain()方法,可以找到最終的執(zhí)行代碼如下:

    public Result run(Runner runner) {
        Result result = new Result();
        RunListener listener = result.createListener();
        notifier.addFirstListener(listener);
        try {
            notifier.fireTestRunStarted(runner.getDescription());
            runner.run(notifier);
            notifier.fireTestRunFinished(result);
        } finally {
            removeListener(listener);
        }
        return result;
    }

可以看到,所有的單元測(cè)試方法都是通過(guò)Runner來(lái)執(zhí)行的。Runner只是一個(gè)抽象類(lèi),它是用來(lái)跑測(cè)試用例并通知結(jié)果的,JUnit提供了很多Runner的實(shí)現(xiàn)類(lèi),可以根據(jù)不同的情況選擇不同的test runner。

5.1.1 @RunWith

通過(guò)@RunWith注解,可以為我們的測(cè)試用例選定一個(gè)特定的Runner來(lái)執(zhí)行。

  • 默認(rèn)的test runner是 BlockJUnit4ClassRunner。
  • @RunWith(JUnit4.class),使用的依然是默認(rèn)的test runner,實(shí)質(zhì)上JUnit4繼承自BlockJUnit4ClassRunner。
5.1.2 Suite

Suite翻譯過(guò)來(lái)是測(cè)試套件,意思是讓我們將一批其他的測(cè)試類(lèi)聚集在一起,然后一起執(zhí)行,這樣就達(dá)到了同時(shí)運(yùn)行多個(gè)測(cè)試類(lèi)的目的。



如上圖所示,假設(shè)我們有3個(gè)測(cè)試類(lèi):TestLogin, TestLogout, TestUpdate,使用Suite編寫(xiě)一個(gè)TestSuite類(lèi),我們可以將這3個(gè)測(cè)試類(lèi)組合起來(lái)一起執(zhí)行。TestSuite類(lèi)代碼如下:

@RunWith(Suite.class)
@Suite.SuiteClasses({
        TestLogin.class,
        TestLogout.class,
        TestUpdate.class
})
public class TestSuite {
    //不需要有任何實(shí)現(xiàn)方法
}

執(zhí)行運(yùn)行TestSuite,相當(dāng)于同時(shí)執(zhí)行了這3個(gè)測(cè)試類(lèi)。
Suite還可以進(jìn)行嵌套,即一個(gè)測(cè)試Suite里包含另外一個(gè)測(cè)試Suite。

@RunWith(Suite.class)
@Suite.SuiteClasses(TestSuite.class)
public class TestSuite2 {
}
5.1.3 Parameterized

我們常規(guī)的測(cè)試方法都是public void修飾的,不能帶有任何輸入?yún)?shù)。但是有時(shí)我們需要在測(cè)試方法里輸入?yún)?shù),甚至可能需要指定批量的參數(shù),如果使用常規(guī)的模式,那就需要為每一種參數(shù)寫(xiě)一個(gè)測(cè)試方法,這顯然不是我們所期望的。使用Parameterized這個(gè)test runner就能實(shí)現(xiàn)這個(gè)目的。

我們有一個(gè)待測(cè)試類(lèi),菲波那切函數(shù),代碼如下:

public class Fibonacci {
    public static int compute(int n) {
        int result = 0;
        if(n <= 1) {
            result = n;
        } else {
            result = compute(n -1) + compute(n - 2);
        }
        return result;
    }
}

針對(duì)這個(gè)函數(shù),我們需要多個(gè)輸入?yún)?shù)來(lái)驗(yàn)證是否正確,來(lái)看看怎么實(shí)現(xiàn)這個(gè)目的。

  • 使用構(gòu)造函數(shù)來(lái)注入?yún)?shù)值
//指定Parameterized作為test runner
@RunWith(Parameterized.class)
public class TestParams {

    //這里是配置參數(shù)的數(shù)據(jù)源,該方法必須是public static修飾的,且必須返回一個(gè)可迭代的數(shù)組或者集合
    //JUnit會(huì)自動(dòng)迭代該數(shù)據(jù)源,自動(dòng)為參數(shù)賦值,所需參數(shù)以及參數(shù)賦值順序由構(gòu)造器決定。
    @Parameterized.Parameters
    public static List getParams() {
        return Arrays.asList(new Integer[][]{
                { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 }
        });
    }

    private int input;
    private int expected;

    //在構(gòu)造函數(shù)里,指定了2個(gè)輸入?yún)?shù),JUnit會(huì)在迭代數(shù)據(jù)源的時(shí)候,自動(dòng)傳入這2個(gè)參數(shù)。
    //例如:當(dāng)獲取到數(shù)據(jù)源的第3條數(shù)據(jù){2,1}時(shí),input=2,expected=1
    public TestParams(int input, int expected) {
        this.input = input;
        this.expected = expected;
    }

    @Test
    public void testFibonacci() {
        System.out.println(input + "," + expected);
        Assert.assertEquals(expected, Fibonacci.compute(input));
    }

}

執(zhí)行該測(cè)試類(lèi),可以看到執(zhí)行過(guò)程中的打印結(jié)果:

0,0
1,1
2,1
3,2
4,3
5,5
6,8
  • 使用@Parameter注解來(lái)注入?yún)?shù)值
@RunWith(Parameterized.class)
public class TestParams2 {

    @Parameterized.Parameters
    public static List getParams() {
        return Arrays.asList(new Integer[][]{
                { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 }
        });
    }

    //這里必須是public,不能是private
    @Parameterized.Parameter
    public int input;

    //注解括號(hào)里的參數(shù),用來(lái)指定參數(shù)的順序,默認(rèn)為0
    @Parameterized.Parameter(1)
    public int expected;

    @Test
    public void testFibonacci() {
        System.out.println(input + "," + expected);
        Assert.assertEquals(expected, Fibonacci.compute(input));
    }

}
5.1.4 Categories

Categories繼承自Suite,但是比Suite功能更加強(qiáng)大,它能對(duì)測(cè)試類(lèi)中的測(cè)試方法進(jìn)行分類(lèi)執(zhí)行。當(dāng)你想把不同測(cè)試類(lèi)中的測(cè)試方法分在一組,Categories就很管用。

  • 先定義category marker類(lèi),它們只是用來(lái)標(biāo)記類(lèi)別的,并不承擔(dān)任何業(yè)務(wù)邏輯。
public interface CategoryMarker {

    public interface FastTests {
        /* category marker */
    }

    public interface SlowTests {
        /* category marker */
    }
}
  • 通過(guò)@Category注解來(lái)標(biāo)記測(cè)試方法的類(lèi)別
public class A {

    @Test
    public void a() {
        System.out.println("method a() called in class A");
    }

    //標(biāo)記該測(cè)試方法的類(lèi)別
    @Category(CategoryMarker.SlowTests.class)
    @Test
    public void b() {
        System.out.println("method b() called in class A");
    }

}
  • 通過(guò)@Category注解來(lái)標(biāo)記測(cè)試類(lèi)的類(lèi)別
@Category({CategoryMarker.FastTests.class, CategoryMarker.SlowTests.class})
public class B {

    @Test
    public void c() {
        System.out.println("method c() called in class B");
    }

}
  • 分類(lèi)執(zhí)行
@RunWith(Categories.class)
@Categories.IncludeCategory(CategoryMarker.SlowTests.class)
@Suite.SuiteClasses({A.class, B.class})  //Categories本身繼承自Suite
public class SlowTestSuite {
    //如果不加@Categories.IncludeCategory注解,效果與Suite一樣
}

//執(zhí)行結(jié)果,打印信息如下:
method b() called in class A
method c() called in class B
@RunWith(Categories.class)
@Categories.IncludeCategory({CategoryMarker.SlowTests.class})    //指定包含的類(lèi)別
@Categories.ExcludeCategory({CategoryMarker.FastTests.class})    //需要排除的類(lèi)別
@Suite.SuiteClasses({A.class, B.class})
public class SlowTestSuite2 {
}

//執(zhí)行結(jié)果,打印信息如下:
method b() called in class A

5.2 @Test的屬性

5.2.1 timeout

timeout用來(lái)測(cè)試一個(gè)方法能不能在規(guī)定時(shí)間內(nèi)完成,當(dāng)為一個(gè)測(cè)試方法指定了timeout屬性后,該方法會(huì)運(yùn)行在一個(gè)單獨(dú)的線程里執(zhí)行。如果測(cè)試方法運(yùn)行時(shí)間超過(guò)了指定的timeout時(shí)間,測(cè)試則會(huì)失敗,并且JUnit會(huì)中斷執(zhí)行該測(cè)試方法的線程。

    //該方法會(huì)在一個(gè)單獨(dú)的線程中執(zhí)行,單位為毫秒,這里超時(shí)時(shí)間為2秒
    @Test(timeout = 2000)
    public void testTimeout() {
        System.out.println("timeout method called in thread " + Thread.currentThread().getName());
    }

    //該方法默認(rèn)會(huì)在主線程中執(zhí)行
    @Test
    public void testNormalMethod() {
        System.out.println("normal method called in thread " + Thread.currentThread().getName());
    }

    //該方法指定了timeout時(shí)間為1秒,實(shí)際運(yùn)行時(shí)會(huì)超過(guò)1秒,該方法測(cè)試無(wú)法通過(guò)
    @Test(timeout = 1000)
    public void testTimeout2() {
        try {
            Thread.sleep(1200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

執(zhí)行后打印結(jié)果如下:

timeout method called in thread Time-limited test
normal method called in thread main
5.2.2 expected

expected屬性是用來(lái)測(cè)試異常的。例如:

    new ArrayList<Object>().get(0);

這段代碼應(yīng)該拋出一個(gè)IndexOutOfBoundsException異常信息,如果我們想驗(yàn)證這段代碼是否拋出了異常,我們可以這樣寫(xiě):

    @Test(expected = IndexOutOfBoundsException.class)
    public void empty() {
        new ArrayList<Object>().get(0);
    }

5.3 Rules

@Rule是JUnit4的新特性,它能夠靈活地?cái)U(kuò)展每個(gè)測(cè)試方法的行為,為他們提供一些額外的功能。下面是JUnit提供的一些基礎(chǔ)的的test rule,所有的rule都實(shí)現(xiàn)了TestRule這個(gè)接口類(lèi)。除此外,可以自定義test rule。

5.3.1 TestName Rule

在測(cè)試方法內(nèi)部能知道當(dāng)前的方法名。

public class NameRuleTest {
   //用@Rule注解來(lái)標(biāo)記一個(gè)TestRule,注意必須是public修飾的
  @Rule
  public final TestName name = new TestName();
  
  @Test
  public void testA() {
    assertEquals("testA", name.getMethodName());
  }
  
  @Test
  public void testB() {
    assertEquals("testB", name.getMethodName());
  }
}
5.3.2 Timeout Rule

與@Test注解里的屬性timeout類(lèi)似,但這里是針對(duì)同一測(cè)試類(lèi)里的所有測(cè)試方法都使用同樣的超時(shí)時(shí)間。

public class TimeoutRuleTest {

    @Rule
    public final Timeout globalTimeout = Timeout.millis(20);

    @Test
    public void testInfiniteLoop1() {
        for(;;) {}
    }

    @Test
    public void testInfiniteLoop2() {
        for(;;) {}
    }
}
5.3.3 ExpectedException Rules

與@Test的屬性expected作用類(lèi)似,用來(lái)測(cè)試異常,但是它更靈活方便。

public class ExpectedExceptionTest {

    @Rule
    public final ExpectedException exception = ExpectedException.none();

    //不拋出任何異常
    @Test
    public void throwsNothing() {
    }

    //拋出指定的異常
    @Test
    public void throwsIndexOutOfBoundsException() {
        exception.expect(IndexOutOfBoundsException.class);
        new ArrayList<String>().get(0);
    }

    @Test
    public void throwsNullPointerException() {
        exception.expect(NullPointerException.class);
        exception.expectMessage(startsWith("null pointer"));
        throw new NullPointerException("null pointer......oh my god.");
    }

}
5.3.4 TemporaryFolder Rule

該rule能夠創(chuàng)建文件以及文件夾,并且在測(cè)試方法結(jié)束的時(shí)候自動(dòng)刪除掉創(chuàng)建的文件,無(wú)論測(cè)試通過(guò)或者失敗。

public class TemporaryFolderTest {

    @Rule
    public final TemporaryFolder folder = new TemporaryFolder();

    private static File file;

    @Before
    public void setUp() throws IOException {
        file = folder.newFile("test.txt");
    }

    @Test
    public void testFileCreation() throws IOException {
        System.out.println("testFileCreation file exists : " + file.exists());
    }

    @After
    public void tearDown() {
        System.out.println("tearDown file exists : " + file.exists());
    }

    @AfterClass
    public static void finish() {
        System.out.println("finish file exists : " + file.exists());
    }

}

測(cè)試執(zhí)行后打印結(jié)果如下:

testFileCreation file exists : true
tearDown file exists : true
finish file exists : false    //說(shuō)明最后文件被刪除掉了
5.3.5 ExternalResource Rules

實(shí)現(xiàn)了類(lèi)似@Before、@After注解提供的功能,能在方法執(zhí)行前與結(jié)束后做一些額外的操作。

public class UserExternalTest {

    @Rule
    public final ExternalResource externalResource = new ExternalResource() {
        @Override
        protected void after() {
            super.after();
            System.out.println("---after---");
        }

        @Override
        protected void before() throws Throwable {
            super.before();
            System.out.println("---before---");
        }
    };

    @Test
    public void testMethod() throws IOException {
        System.out.println("---test method---");
    }

}

執(zhí)行后打印結(jié)果如下:

---before---
---test method---
---after---
5.3.6 Custom Rules

自定義rule必須實(shí)現(xiàn)TestRule接口。
下面我們來(lái)寫(xiě)一個(gè)能夠讓測(cè)試方法重復(fù)執(zhí)行的rule:

public class RepeatRule implements TestRule {

    //這里定義一個(gè)注解,用于動(dòng)態(tài)在測(cè)試方法里指定重復(fù)次數(shù)
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    public @interface Repeat {
        int count();
    }

    @Override
    public Statement apply(final Statement base, final Description description) {
        Statement repeatStatement =  new Statement() {
            @Override
            public void evaluate() throws Throwable {
                Repeat repeat = description.getAnnotation(Repeat.class);
                //如果有@Repeat注解,則會(huì)重復(fù)執(zhí)行指定次數(shù)
                if(repeat != null) {
                    for(int i=0; i < repeat.count(); i++) {
                        base.evaluate();
                    }
                } else {
                    //如果沒(méi)有注解,則不會(huì)重復(fù)執(zhí)行
                    base.evaluate();
                }
            }
        };
        return repeatStatement;
    }
}
public class RepeatTest {

    @Rule
    public final RepeatRule repeatRule = new RepeatRule();

    //該方法重復(fù)執(zhí)行5次
    @RepeatRule.Repeat(count = 5)
    @Test
    public void testMethod() throws IOException {
        System.out.println("---test method---");
    }

    @Test
    public void testMethod2() throws IOException {
        System.out.println("---test method2---");
    }
}

執(zhí)行結(jié)果如下:

---test method2---
---test method---
---test method---
---test method---
---test method---
---test method---

5.4 小結(jié)

本文主要介紹了JUnit自身提供的幾個(gè)主要的test runner,還有@Test的timeout跟expected屬性,最后介紹了一些常規(guī)的Rules使用方法。test runner是一個(gè)很重要的概念,因?yàn)樵贏ndroid中進(jìn)行單元測(cè)試時(shí),通常都是JUnit結(jié)合其他測(cè)試框架來(lái)一起完成,例如mockito、robolectric,它們都有自己實(shí)現(xiàn)的一套test runner,我們必須使用這些框架提供的test runner才能發(fā)揮這些框架的最大作用。

系列文章:
Android單元測(cè)試(一):前言
Android單元測(cè)試(二):什么是單元測(cè)試
Android單元測(cè)試(三):測(cè)試難點(diǎn)及方案選擇
Android單元測(cè)試(四):JUnit介紹
Android單元測(cè)試(五):JUnit進(jìn)階
Android單元測(cè)試(六):Mockito學(xué)習(xí)
Android單元測(cè)試(七):Robolectric介紹
Android單元測(cè)試(八):怎樣測(cè)試異步代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。