前面一章介紹了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è)試異步代碼