Android Test Base--JUnit Framework
JUnit Intro
Android基於JUnit Framework來書寫測試程式碼。JUnit是基於Java語言的流行的、廣泛被使用的測試框架,當前最新的版本是JUnit4。相對於之前的版本而言,JUnit4允許開發者使用更清晰、簡潔、靈活的方式來構建測試程式碼。Android也提供相應更新支援JUnit4,並建議開發者基於JUnit4來寫測試程式碼。因此,我們這裡主要學習JUnit4的使用。
JUnit的一個測試類就是一個普通的Java Class。通過使用JUnit4提供的註解可以快速便捷的構建Test Class。e.g.
public class CalculatorTest {
private Calculator calculator;
@BeforeClass
public static void setUpBeforeClass(){
}
@Before
public void setupBeforeTest(){
calculator = new Calculator();
}
@Test
public void testPlus() throws Exception {
assertEquals(calculator.plus(2, 3), 5);
}
@Test
public void testMinus() throws Exception {
assertEquals(calculator.minus(5, 3), 2);
}
@Test
public void testMultiply() throws Exception {
assertEquals(calculator.multiply(2, 3), 6);
}
@Test
public void testDivide(){
assertEquals(calculator.divide(10, 2), 5);
}
@After
public void cleanupAfterTest(){
calculator = null;
}
@AfterClass
public static void cleanupAfterClass(){
}
}
上面的程式碼展示了一個簡單的Test Class。包含了最常用的註解,註解名稱代表的含義一目瞭然,說明如下:
-
@Test:方法註解,要求方法簽名為
public void
。宣告該方法為一個Test Case Method。一個Test Class可以包含若干個這樣的方法,JUnit 會依次執行這些方法。該註解中又包含兩個可配置的值:- timeout:設定一個單位為毫秒的時間值,如果該測試方法執行的時間超過該指定值,則該測試方法失敗;一般用來捕獲或者終止迴圈;e.g.
@Test(timeout=100) public void infinity() { while(true); }
- *exception*:指定對應的測試方法會丟擲某個異常;如果該測試方法沒有丟擲指定的異常,則測試不通過;e.g.
@Test(expected = ArithmeticException.class) public void testDivideExpectException(){ calculator.divide(10, 0); }
-
@Before :方法註解,要求方法簽名為
public void
。每一個Test Class中允許包含多個這種方法,對應方法會在執行該Test Class中的每個test method之前呼叫。 - @After :同@Before對應,只是呼叫時機不同,該方法會在該Test Class中每個test method執行完成之後呼叫。
-
@BeforeClass :方法註解,要求方法簽名為
public static void
。該方法會在執行該Test Class時被呼叫且只會被呼叫一次;一般在該方法中初始化一些全域性的、開銷比較大的操作,比如初始化資料庫連線等。 - @AfterClass :同@BeforeClass註解對應,只是呼叫時機不同,該方法會在Test Class執行完所有的Test method後呼叫且只會呼叫一次;在該方法中可以做一些清理的工作。
Base Concept
-
Runner類:JUnit將如何執行一個Test Class抽象為一個
Runner
類 。JUnit4提供了一個基礎的抽象Runner子類ParentRunner<T>
。
public abstract class ParentRunner<T> extends Runner implements Filterable, Sortable {
......
/** * Returns a list of objects that define the children of this Runner. */
protected abstract List<T> getChildren();
protected abstract void runChild(T child, RunNotifier notifier);
.....
}
該類是一個泛型類,可以將ParentRunner看成是一棵Test Tree的父親節點,對應的型別引數T 就是代表其下的子節點的型別。針對一個具體的Test Class,ParentRunner層負責處理@BeforeClass、@AfterClass和@ClassRule註解的方法,遍歷並執行所有的child。JUnit允許自定義Runner,通過@RunWith註解可以指定一個Test Class使用某個Runner。
-
Statement:抽象類,代表了一個Test Class執行過程中的一個或多個動作,通過
evaluate()
方法來執行這些動作。類似於Java中的Runnable介面,evaluate()
就相當於run()
方法。
public abstract class Statement {
/** Run the action, throwing a Throwable if anything goes wrong. */
public abstract void evaluate() throws Throwable;
}
How JUnit Run?
-
Build Runner
我們可以很方便的通過IDE來執行我們的Test Class,也可以自己通過命令列工具執行。因為本質上我們只是執行了一個普通的Java程式而已,IDE只不過是幫我們寫好了命令、封裝好引數而已。
java -cp .;C:\Users\Administrator.gradle\caches\modules-2\files-2.1\junit\junit\4.12\2973d150c0dc1fefe998f834810d68f278ea58ec\junit-4.12.jar;D:\AndroidCode\StudioCode\AndroidTestPractice\app\build\intermediates\classes\test\debug;D:\AndroidCode\StudioCode\AndroidTestPractice\app\build\intermediates\classes\debug;D:\AndroidCode\StudioCode\AndroidTestPractice\build\generated\mockable-android-23.jar;C:\Users\Administrator.gradle\caches\modules-2\files-2.1\org.hamcrest\hamcrest-core\1.3\42a25dc3219429f0e5d060061f71acb49bf010a0\hamcrest-core-1.3.jar; org.junit.runner.JUnitCore com.lcd.androidtestpractice.ExampleUnitTest com.lcd.androidtestpractice.CalculatorTest
上面的命令指定執行ExampleUnitTest和CalculatorTest兩個Test Class。-cp
後面指定class path,多個path之間使用;
分號隔開。我們需要指定所有需要的classpath(包括JUnit的jar路徑、JUnit所依賴的其他jar包的路徑、Test Classes路徑和其依賴的其他所有類路徑),需要指定執行JUnit的入口類org.junit.runner.JUnitCore
以及指定我們需要執行的Test Classes。那麼長的執行語句,想想還是用IDE吧
不管是IDE還是CMD,最終的統一入口都是JUnitCore
。在其main方法中會解析引數,這些引數其實就是我們需要執行的所有Test Class的全域限定名稱。JUnit將這些引數封裝成為一個Request物件執行,其實就是從request中取出Runner呼叫run方法執行,這些步驟比較簡單,在此略過分析。
```java
public class JUnitCore {
private final RunNotifier notifier = new RunNotifier();
...
public static void main(String... args) {
Result result = new JUnitCore().runMain(new RealSystem(), args);
System.exit(result.wasSuccessful() ? 0 : 1);
}
public Result run(Request request) {
return run(request.getRunner());
}
...
}
```
我們先來看看Runner的實現到底是個什麼物件,如何構造的?因為具體的程式碼比較簡單,這裡我就不一一分析了,有興趣的同學可以自己閱讀原始碼。 我直接給出答案:JUnit通過RunnerBuilder
類來針對具體的Test Class為其構造具體的Runner實現。且在JUnit4中提供AllDefaultPossibilitiesBuilder
類為預設使用的builder。
public class AllDefaultPossibilitiesBuilder extends RunnerBuilder {
private final boolean canUseSuiteMethod;
public AllDefaultPossibilitiesBuilder(boolean canUseSuiteMethod) {
this.canUseSuiteMethod = canUseSuiteMethod;
}
@Override
public Runner runnerForClass(Class<?> testClass) throws Throwable {
List<RunnerBuilder> builders = Arrays.asList(
ignoredBuilder(),
annotatedBuilder(),
suiteMethodBuilder(),
junit3Builder(),
junit4Builder());
for (RunnerBuilder each : builders) {
Runner runner = each.safeRunnerForClass(testClass);
if (runner != null) {
return runner;
}
}
return null;
}
protected JUnit4Builder junit4Builder() {
return new JUnit4Builder();
}
protected JUnit3Builder junit3Builder() {
return new JUnit3Builder();
}
protected AnnotatedBuilder annotatedBuilder() {
return new AnnotatedBuilder(this);
}
protected IgnoredBuilder ignoredBuilder() {
return new IgnoredBuilder();
}
protected RunnerBuilder suiteMethodBuilder() {
if (canUseSuiteMethod) {
return new SuiteMethodBuilder();
}
return new NullBuilder();
}
}
程式碼很清晰,通過Builder的runnerForClass(Class<?> testClass)
方法為一個具體的Test Class構建對應的Runner。該方法會依次遍歷AllDefaultPossibilitiesBuilder
中內建的Builders,呼叫每個builder的safeRunnerForClass
方法請求為該Test Class生成Runner。如果返回為null,則說明這個builder無法為此Test Class構建Runner,繼續遍歷其他Builder。否則返回。
-
IgnoredBuilder:該Builder檢查Test Class是否使用@Ignore註解修飾,如果是,返回
IgnoredClassRunner
,否則返回null。IgnoredClassRunner
在執行時,其實什麼都沒幹,等於就是忽略執行這個Test Class。public class IgnoredBuilder extends RunnerBuilder { @Override public Runner runnerForClass(Class<?> testClass) { if (testClass.getAnnotation(Ignore.class) != null) { return new IgnoredClassRunner(testClass); } return null; }
}
```
- AnnotatedBuilder:該Builder檢查Test Class是否使用@RunWith註解修飾,如果有,會通過反射構建對應的Runner物件返回,否則返回null。這裡不再給出具體的程式碼。
- SuiteMethodBuilder和JUnit3Builder:這兩個Builder是相容老版本的,用來構建基於JUnit3的Runner,不再討論。
-
JUnit4Builder:構建基於JUnit4的runner,該builder直接返回一個
BlockJUnit4ClassRunner
的物件。所以如果前面的Builder都沒有能夠為Test Class構建Runner,則這個就是其預設的Runner。
到現在為止,Runner物件已經構建並且返回。直接呼叫它的run()
方法就相當於執行這個Test Class。
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;
}
ParentRunner.run() Flow
上面說到了Runner的run方法。在JUnit4中的Runner一般為ParentRunner的子類,所以相應的這裡從ParentRunner的
run(final RunNotifier notifier)
方法開始分析。首先通過classBlock(notifier)
方法返回一個statement
物件,包含了一系列要執行的動作,直接呼叫evaluate()
方法執行。
@Override
public void run(final RunNotifier notifier) {
...
Statement statement = classBlock(notifier);
statement.evaluate();
...
}
- 那這個statement物件是怎麼構造的呢?具體裡面包含哪些執行動作?
protected Statement classBlock(final RunNotifier notifier) {
Statement statement = childrenInvoker(notifier);
if (!areAllChildrenIgnored()) {
statement = withBeforeClasses(statement);
statement = withAfterClasses(statement);
statement = withClassRules(statement);
}
return statement;
}
- 首先是通過
childrenInvoker
方法封裝執行所有child的動作到statement中,該動作等於是呼叫runChildren
方法。
protected Statement childrenInvoker(final RunNotifier notifier) {
return new Statement() {
@Override
public void evaluate() {
runChildren(notifier);
}
};
}
- 然後是通過
withBeforeClasses()
方法來封裝@BeforeClass註解修飾的方法,如果Test Class中存在使用@BeforeClass註解修飾的方法,則new一個RunBefores
物件返回,否則直接返回原來的statement物件。
protected Statement withBeforeClasses(Statement statement) {
List<FrameworkMethod> befores = testClass
.getAnnotatedMethods(BeforeClass.class);
return befores.isEmpty() ? statement :
new RunBefores(statement, befores, null);
}
public class RunBefores extends Statement {
private final Statement next;
private final Object target;
private final List<FrameworkMethod> befores;
public RunBefores(Statement next, List<FrameworkMethod> befores, Object target) {
this.next = next;
this.befores = befores;
this.target = target;
}
@Override
public void evaluate() throws Throwable {
for (FrameworkMethod before : befores) {
before.invokeExplosively(target);
}
next.evaluate();
}
}
RunBefores
繼承於Statement,如果返回的是RunBefores
物件,當執行其evaluate()
方法時,會先執行所有的befores指定的動作,即執行所有的@BeforeClass修飾的方法,之後執行next指代的動作,這裡next指代的其實就是上一步的runChildren
動作。
- 同理,之後是通過
withAfterClasses()
方法加入@AfterClass對應的動作。如果Test Class中存在使用@AfterClass註解修飾的方法,則new一個RunAfters
物件返回,否則直接返回原來的statement物件。
protected Statement withAfterClasses(Statement statement) {
List<FrameworkMethod> afters = testClass
.getAnnotatedMethods(AfterClass.class);
return afters.isEmpty() ? statement :
new RunAfters(statement, afters, null);
}
public class RunAfters extends Statement {
private final Statement next;
private final Object target;
private final List<FrameworkMethod> afters;
public RunAfters(Statement next, List<FrameworkMethod> afters, Object target) {
this.next = next;
this.afters = afters;
this.target = target;
}
@Override
public void evaluate() throws Throwable {
...
next.evaluate();
...
for (FrameworkMethod each : afters) {
each.invokeExplosively(target);
}
...
}
}
RunAfters
同樣繼承於Statement,如果返回的是RunAfters
物件,當執行其evaluate()
方法時,會先執行next指代的動作,即先執行上一步的動作。然後才會執行所有的afters指定的動作,即執行所有的@AfterClass修飾的方法。
- 下一步,通過
withClassRules
方法新增ClassRule中的動作。
private Statement withClassRules(Statement statement) {
List<TestRule> classRules = classRules();
return classRules.isEmpty() ? statement :
new RunRules(statement, classRules, getDescription());
}
這裡有必要先來了解一下什麼是TestRule
。先看它的類定義:
public interface TestRule {
Statement apply(Statement base, Description description);
}
TestRule允許我們在Test Class執行過程中插入自定義的一些操作。具體的可以在base statement執行的前後加入一些其他操作邏輯。下面的程式碼自定義一個Rule,該Rule先執行doSomethingBefore()
方法,然後執行statement,最後執行doSomethingAfter()
方法。e.g.
public class ClassRuleTest {
@ClassRule
public static TestRule myRule(){
return new TestRule() {
private void doSomethingBefore(){}
private void doSomethingAfter(){}
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
doSomethingBefore(); //do something before base actions
base.evaluate();
doSomethingAfter(); //do something after base actions
}
};
}
};
}
}
TestRule又分為類級別的Rule和例項級別的Rule。
- @ClassRule:用來宣告一個類級別的Rule,可修飾方法和欄位;當修飾方法時,要求方法簽名為public static
且方法的返回型別為TestRule
或者其子型別;同樣的,修飾變數時,要求變數為public static
且型別為TestRule
或者其子型別;JUnit在搜尋ClassRule時,會先查詢符合條件的方法,並呼叫,將返回值新增到TestRule
列表中;然後搜尋符合條件的Field欄位,同樣加入列表中。
- @Rule:用來宣告一個例項級別的Rule,可修飾方法和欄位;方法要求為public
且返回型別為TestRule
或者其子型別;修飾變數時要求為public
的例項變數且型別為TestRule
或者其子型別;JUnit同樣會在適當的時機搜尋Test Class中的所有例項Rule並運用。
現在回到上面的流程,如果Test Class中不存在類級別的Rule,則直接返回上一步的statement物件;否則構建一個`RunRules`物件返回。`RunRules`同樣繼承於`Statement`,在其建構函式中會遍歷所有的ClassRule並呼叫apply方法返回一個新的statement,這樣就給每個Rule提供了在base statement基礎上插入自定義操作的機會。上面的分析,其實主要涉及的是Test Class類層面的操作。現在我們先來梳理一下最終返回的class statement可能包含的操作和其執行流程流程:
這裡我們假設ClassRule、BeforeClass和AfterClass都存在,如果某一項不存在,則只需忽略掉流程圖中對應的部分即可。因為TestRule的特殊性,某個TestRule可能在Statement前後都新增了自定義操作,所以流程圖中ClassRule將對應兩個部分。當然,某個TestRule可能只在base statement基礎操作前新增自定義操作,那麼其對應的後置操作部分相當於什麼都沒幹;反之亦然!這裡Statement物件的層層巢狀,其實是使用了設計模式中的裝飾器模式,感興趣的同學可以私下了解一下。
-
runChildren
下面我們來分析runChildren的流程。runChildren會首先獲取Children列表,然後遍歷並在每個Child上呼叫runChild方法。runChild方法在ParentRunner中是一個抽象方法,由具體的subclass類來實現。
private void runChildren(final RunNotifier notifier) {
final RunnerScheduler currentScheduler = scheduler;
try {
for (final T each : getFilteredChildren()) {
currentScheduler.schedule(new Runnable() {
public void run() {
ParentRunner.this.runChild(each, notifier);
}
});
}
} finally {
currentScheduler.finished();
}
}
JUnit4提供了ParentRunner的兩個直接實現BlockJUnit4ClassRunner
和Suite
。
-
BlockJUnit4ClassRunner:JUnit4預設Ruuner,執行Test Class下的所有Test method。該Runner包含的每個Child其實是一個
FrameworkMethod
物件,代表一個@Test方法。
public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> {
@Override
protected List<FrameworkMethod> getChildren() {
return computeTestMethods();
}
/**
* Returns the methods that run tests. Default implementation returns all
* methods annotated with {@Test} on this class and superclasses that
* are not overridden.
*/
protected List<FrameworkMethod> computeTestMethods() {
return getTestClass().getAnnotatedMethods(Test.class);
}
...
}
可以看到,getChildren()方法返回的Children列表其實是FrameworkMethod
物件列表,即Test Class中所有使用@Test註解修飾的方法列表。在ParentRunner中呼叫runChildren時,其實是在每個@Test方法上呼叫runChild方法。在該方法中,首先檢查是否忽略,其實就是檢查是否有@Ignore註解;如果忽略,則該Test Method得不到執行;否則,通過methodBlock(method)
方法返回一個Statement物件,並呼叫runLeaf方法執行這個statement物件。
protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
Description description = describeChild(method);
if (isIgnored(method)) {
notifier.fireTestIgnored(description);
} else {
runLeaf(methodBlock(method), description, notifier);
}
}
protected final void runLeaf(Statement statement, Description description,
RunNotifier notifier) {
...
statement.evaluate();
...
}
那這個statement物件裡面又封裝了哪些操作呢?來看methodBlock方法怎麼構造這個物件:
protected Statement methodBlock(FrameworkMethod method) {
Object test;
try {
test = new ReflectiveCallable() {
@Override
protected Object runReflectiveCall() throws Throwable {
return createTest();
}
}.run();
} catch (Throwable e) {
return new Fail(e);
}
Statement statement = methodInvoker(method, test);
statement = possiblyExpectingExceptions(method, test, statement);
statement = withPotentialTimeout(method, test, statement);
statement = withBefores(method, test, statement);
statement = withAfters(method, test, statement);
statement = withRules(method, test, statement);
return statement;
}
首先,通過createTest()
方法直接構造了一個Test Class的例項test。createTest()
方法使用的是反射的方式構建例項物件,使用的是Test Class預設建構函式,如果Test Class類中宣告有其他的建構函式,請保證預設的建構函式存在,否則會因為無法建立例項而丟擲異常。無法然後才開始處理Statement。這裡我們看到了類似ParentRunner中的處理邏輯,一層層的statement的巢狀。首先methodInvoker
返回的基礎statement封裝了在建立的例項test上執行@Test方法的操作,接著是封裝@Test
中宣告的expected異常和timeout的處理邏輯,再然後就是withBefores封裝所有的@Before方法操作、withAfters封裝所有@After方法操作和withRules封裝所有的@Rule操作。這裡跟前面已經分析過的@BeforeClass、@AfterClass和@ClassRule操作的封裝原理是相通的,就不一一貼出程式碼說明了。
- Suite:該Runner允許將多個Test Class通過@SuiteClasses註解宣告為一個測試套件來執行;當執行該Test Class時,會依次執行註解中包含的所有Test Class。一般我們可以通過如下的方式使用
@RunWith(Suite.class)
@SuiteClass({TestClass1.class, TestClass2.class...})
public class MySuite {}
通過@RunWith註解,在為該MySuite這個Test Class構建具體的Runner時,返回的就是Suite物件。
public class Suite extends ParentRunner<Runner> {
...
private final List<Runner> runners;
@Override
protected List<Runner> getChildren() {
return runners;
}
@Override
protected void runChild(Runner runner, final RunNotifier notifier) {
runner.run(notifier);
}
...
}
Suite中的每一個Child都是一個Runner,因為Suite中包含了一系列的Test Classes,Suite物件在構建的時候,就會為這些Test Class都構建相應Runner(Runner構建的流程參照上面的說明);這些Runners作為Suite的Children儲存到runners列表中。當在ParentRunner中呼叫runChildren時,其實就是在這些Runner物件上依次呼叫runChild方法。而runChild方法實現很簡單,直接交給對應的Runner來處理。所以Suite並不關心如何去執行它包含的每個Test Class,真正的執行還是由Test Class自己決定的。到這裡,我們runChildren的執行邏輯已經分析完了。根據上面的分析,下面給出相應的流程圖來說明最終每個child statement具體的執行過程:
child statement的執行流程對照class statement的流程。這裡也是假設Rule、Before和After都存在,如果不存在,請忽略圖中對應的部分。
相關文章
- Test Generation frameworkFramework
- 【Android Test】糟心的“Empty test suite ”異常AndroidUI
- Android FrameworkAndroidFramework
- Android Framework核心之旅AndroidFramework
- Android Framework中的Application Framework層介紹AndroidFrameworkAPP
- Android Framework : Alarm 機制AndroidFramework
- 我眼中的Android FrameworkAndroidFramework
- Android Framework: 增加trace點AndroidFramework
- Android Media Framework - 開篇AndroidFramework
- Android framework中使用stl庫AndroidFramework
- Android Media Framework(五)Tunnel ModeAndroidFramework
- Android 測試入門之---Monkey testAndroid
- Android FrameWork 之原始碼編譯AndroidFramework原始碼編譯
- 如何反編譯Android 5.0 framework編譯AndroidFramework
- test
- android 8.0 Autofill Framework (自動填充框架)AndroidFramework框架
- Android Framework層JNI的使用淺析AndroidFramework
- Android Media Framework(一)OpenMAX 框架簡介AndroidFramework框架
- webservice testWeb
- Unit test
- test_NO
- Android FrameWork學習(二)Android系統原始碼除錯AndroidFramework原始碼除錯
- android studio 除錯 framework 層程式碼Android除錯Framework
- [Android Framework]獲取U盤 SD 狀態AndroidFramework
- android framework中新增自定義許可權AndroidFramework
- Android Media Framework(三)OpenMAX API閱讀與分析AndroidFrameworkAPI
- Shell test 命令
- JavaScript test() 方法JavaScript
- Spring testSpring
- ACM Coin TestACM
- partition table test
- test004
- test日記
- test6
- WPF test GPUGPU
- test2
- 1-test
- test3