天天看點

單元測試以及JUnit架構解析

前言

我們都有個習慣,常常不樂意去寫個簡單的單元測試程式來驗證自己的代碼。對自己的程式一直非常有自信,或存在僥幸心理每次運作通過後就直接扔給測試組測試了。然而每次測試組的BUG送出過來後就會發現自己的程式還存在許多沒有想到的漏洞。但是每次修改好BUG以後還是懷着僥幸心理,認為這次不會有bug了。然後又一次自信地送出,結果又敗了。因為這樣反複幾次後。開發者花在找BUG和修複BUG的這些時間加起來已經比他開發這個子產品花的時間還要多了。雖然項目經理已經預留了修改BUG和單元測試的時間。但是開發者卻習慣性地在寫好代碼後就認為任務完成了。 然後等問題出來了bug改了很多次還是修複不了的時候才和項目經理說“我碰到預想不到的問題,可能要延期釋出我的代碼“。如果這個項目不可延期,痛苦的加班就無法避免了。

BUG是不可避免的,隻是每次在修複一個BUG之前基本上無法知道這個BUG是哪段代碼引起。每次定位BUG可能會耗去你一個小時還是一天,這還要取決于你的水準了。但是如果你的每段核心程式都有單元測試代碼。你将不需要靠你的經驗去判斷或猜測BUG是由哪段程式引起。你隻要運作你的單元測試方法。通過簡單判斷測試方法的結果就可以輕松定位BUG了。是以從表面上看,為每個單元程式都編寫測試代碼似乎是增加了工作量,但是其實這些代碼不僅為你織起了一張保護網,而且還可以幫助你快速定位錯誤進而使你大大減少修複BUG的時間。而且這還有利你的身體健康,你将不會因為找不出BUG而痛苦不已,也将不用廢寝忘食地加班了。而且項目的進度也将盡在掌握。

其實單元測試不僅能保證項目進度還能優化你的設計。有些開發者會說,寫單元測試代碼太費勁了,比寫業務代碼還麻煩。可是如果強迫開發者必須寫單元測試代碼的時候。聰明且又想‘偷懶’的開發人員為了将來可以更友善地編寫測試代碼。唯一的辦法就是通過優化設計,盡可能得将業務代碼設計成更容易測試的代碼。慢慢地開發者就會發現。自己設計的程式耦合度也越來越低。每個單元程式的輸入輸出,業務内容和異常情況都會盡可能變得簡單。最後發現自己的程式設計習慣和設計能力也越來越老練了。

其實容易測試的代碼基本上可以和設計良好的代碼劃等号。因為一個單元測試用例其實就是一個單元的最早使用者。容易使用顯然意味着良好的設計。

什麼是單元測試

單元測試的目的 測試目前所寫的代碼是否是正确的, 例如輸入一組資料, 會輸出期望的資料; 輸入錯誤資料, 會産生錯誤異常等。

在單元測試中, 我們需要保證被測系統是獨立的,即當被測系統通過測試時,那麼它在任何環境下都是能夠正常工作的。編寫單元測試時, 僅僅需要關注單個類就可以了,而不需要關注例如資料庫服務、Web 服務等元件。

JUnit子產品和說明

子產品 說明
Assertions 斷言,單元測試中不可或缺的組成部分
Test Runners 應該如何執行測試
Aggregating tests in Suites 如何将多個相關測試組合到一個測試套件中
Test Execution Order 指定運作單元測試的順序
Exception Testing 如何在單元測試中指定預期的異常
Matchers and assertThat 如何使用Hamcrest比對器和更具描述性的斷言
Ignoring Tests 如何禁用測試方法或類
Timeout for Tests 如何指定測試的最長執行時間
Parameterized Tests 編寫可以使用不同參數值多次執行的測試
Assumptions with Assume 類似于斷言,但沒有使測試失敗
Rules 停止擴充抽象測試類并開始編寫測試規則
Theories 使用随機生成的資料編寫更像科學實驗的測試
Test Fixtures 在每個方法和每個類的基礎上指定設定和清理方法
Categories 将測試分組在一起以便于測試過濾
Multithreaded code and Concurrency 并發代碼測試的基本思路

JUnit4 注解

  • @BeforeClass 表示該方法隻執行一次,并且在所有方法之前執行。一般可以使用該方法進行資料庫連接配接操作,注意該注解運用在靜态方法。
  • @AfterClass 表示該方法隻執行一次,并且在所有方法之後執行。一般可以使用該方法進行資料庫連接配接關閉操作,注意該注解運用在靜态方法。
  • @Before 表示該方法在每一個測試方法之前運作,可以使用該方法進行初始化之類的操作
  • @After 表示該方法在每一個測試方法之後運作,可以使用該方法進行釋放資源,回收記憶體之類的操作
以上4個注解隻能修飾方法,對應子產品是

Test Fixtures

。用于執行測試用例之前,對資源的初始化以及資源清理等工作。這麼做的目的是為了避免多個測試用例互相影響。
  • @Rule
  • @ClassRule
以上2個注解可以修飾域和方法,對應子產品是

Rules

。加

Class

的目的用于修飾

static

域或方法。
  • @Ignore
當需要臨時禁用一個/組測試用例時,可以在已經标注@Test的方法中繼續标注@Ignore,則該測試用例會在執行時被忽略。
  • @FixMethodOrder
此類允許使用者選擇測試類内方法的執行順序。
  • @Test
@Test 修飾

public

(Junit5 以後能支援包通路權限)的方法,但凡測試用例抛出不可預期的異常即認定為測試用例執行失敗。

使用教程

Assume

假設是在斷言之前增加前提條件,隻有當條件成立時斷言才會執行。

否則會抛出假設不通過的異常(但不會判定為測試用例失敗,而是認為是忽略)。

import static org.junit.Assume.*
    @Test public void filenameIncludesUsername() {
        assumeThat(File.separatorChar, is('/'));
        assertThat(new User("optimus").configFileName(), is("configfiles/optimus.cfg"));
    }

    @Test public void correctBehaviorWhenFilenameIsNull() {
       assumeTrue(bugFixed("13356"));  // bugFixed is not included in JUnit
       assertThat(parse(null), is(new NullDocument()));
    }           

複制

Assert

JUnit為所有原始類型、對象和數組(原語或對象)提供了重載斷言方法。參數順序是期望值,其次是實際值。可選地,第一個參數可以是在失敗時輸出的字元串消息。

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.both;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.everyItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import java.util.Arrays;

import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;

public class AssertTests {
  @Test
  public void testAssertArrayEquals() {
    byte[] expected = "trial".getBytes();
    byte[] actual = "trial".getBytes();
    assertArrayEquals("failure - byte arrays not same", expected, actual);
  }

  @Test
  public void testAssertEquals() {
    assertEquals("failure - strings are not equal", "text", "text");
  }

  @Test
  public void testAssertFalse() {
    assertFalse("failure - should be false", false);
  }

  @Test
  public void testAssertNotNull() {
    assertNotNull("should not be null", new Object());
  }

  @Test
  public void testAssertNotSame() {
    assertNotSame("should not be same Object", new Object(), new Object());
  }

  @Test
  public void testAssertNull() {
    assertNull("should be null", null);
  }

  @Test
  public void testAssertSame() {
    Integer aNumber = Integer.valueOf(768);
    assertSame("should be same", aNumber, aNumber);
  }

  // JUnit Matchers assertThat
  @Test
  public void testAssertThatBothContainsString() {
    assertThat("albumen", both(containsString("a")).and(containsString("b")));
  }

  @Test
  public void testAssertThatHasItems() {
    assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
  }

  @Test
  public void testAssertThatEveryItemContainsString() {
    assertThat(Arrays.asList(new String[] { "fun", "ban", "net" }), everyItem(containsString("n")));
  }

  // Core Hamcrest Matchers with assertThat
  @Test
  public void testAssertThatHamcrestCoreMatchers() {
    assertThat("good", allOf(equalTo("good"), startsWith("good")));
    assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
    assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
    assertThat(7, not(CombinableMatcher.<Integer> either(equalTo(3)).or(equalTo(4))));
    assertThat(new Object(), not(sameInstance(new Object())));
  }

  @Test
  public void testAssertTrue() {
    assertTrue("failure - should be true", true);
  }
}           

複制

當測試用例需要驗證異常抛出時

方法一,這個方法的缺陷是無法驗證是在哪一個環節抛出的異常,是以個人不推薦使用。

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

複制

方法二,try/catch方式,這種方式的缺點是代碼量多

@Test
public void testExceptionMessage() {
    try {
        new ArrayList<Object>().get(0);
        fail("Expected an IndexOutOfBoundsException to be thrown");
    } catch (IndexOutOfBoundsException anIndexOutOfBoundsException) {
        assertThat(anIndexOutOfBoundsException.getMessage(), is("Index: 0, Size: 0"));
    }
}           

複制

方法三,使用内置的

@ExpectedException

,個人比較推薦

@Rule
public ExpectedException thrown = ExpectedException.none();

@Test
public void shouldTestExceptionMessage() throws IndexOutOfBoundsException {
    List<Object> list = new ArrayList<Object>();
 
    thrown.expect(IndexOutOfBoundsException.class);
    thrown.expectMessage("Index: 0, Size: 0");
    list.get(0); // execution will never get past this line
}           

複制

Matchers and assertThat

表達形式如下:

assertThat([value], [matcher statement]);

assertThat(x, is(3));
assertThat(x, is(not(4)));
assertThat(responseString, either(containsString("color")).or(containsString("colour")));
assertThat(myList, hasItem("3"));           

複制

它的好處是非常靈活,并且外部有擴充實作(

org.hamcrest

)可以無縫使用。

雖然對開發人員來說,這套

Matchers

的設計顯得有些畫蛇添足。但對測試人員來講,這套設計可以減少很多麻煩。

按需取用即可。

需要參數的測試用例

我們都知道

@Test

修飾方法是不能加參數的,否則在執行時會抛出異常。但是的确存在需要參數的情況,可以使用以下方式進行實作。

import static org.junit.Assert.assertEquals;

import java.util.Arrays;
import java.util.Collection;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class FibonacciTest {
    @Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] {     
                 { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 }  
           });
    }

    private int fInput;

    private int fExpected;

    public FibonacciTest(int input, int expected) {
        this.fInput = input;
        this.fExpected = expected;
    }

    @Test
    public void test() {
        assertEquals(fExpected, Fibonacci.compute(fInput));
    }
}           

複制

示例代碼實作了使用7組參數輸入,來驗證斐波那契數列的合法性。

延伸:Mock or Stub

在做單元測試的時候,我們會發現我們要測試的方法會引用很多外部依賴的對象,比如:(發送郵件,網絡通訊,記錄Log, 檔案系統 之類的)。 而我們沒法控制這些外部依賴的對象。 為了解決這個問題,我們需要用到Stub和Mock來模拟這些外部依賴的對象,進而控制它們。

JUnit是單元測試架構,可以輕松的完成關聯依賴關系少或者比較簡單的類的單元測試,但是對于關聯到其它比較複雜的類或對運作環境有要求的類的單元測試,模拟環境或者配置環境會非常耗時,實施單元測試比較困難。而這些“mock架構”(Mockito 、jmock 、 powermock、EasyMock),可以通過mock架構模拟一個對象的行為,進而隔離開我們不關心的其他對象,使得測試變得簡單。(例如service調用dao,即service依賴dao,我們可以通過mock dao來模拟真實的dao調用,進而能達到測試service的目的。)

模拟對象(Mock Object)可以取代真實對象的位置,用于測試一些與真實對象進行互動或依賴于真實對象的功能,模拟對象的背後目的就是建立一個輕量級的、可控制的對象來代替測試中需要的真實對象,模拟真實對象的行為和功能。

Mockito簡單運用說明

  • when(mock.someMethod()).thenReturn(value)

    設定mock對象某個方法調用時的傳回值。可以連續設定傳回值,即

    when(mock.someMethod()).thenReturn(value1).thenReturn(value2)

    ,第一次調用時傳回

    value1

    ,第二次傳回

    value2

    。也可以表示為如下:

    when(mock.someMethod()).thenReturn(value1,value2)

  • ② 調用以上方法時抛出異常:

    when(mock.someMethod()).thenThrow(new RuntimeException());

  • ③ 另一種stubbing文法:

    doReturn(value).when(mock.someMethod()) doThrow(new RuntimeException()).when(mock.someMethod())

  • ④ 對void方法進行方法預期設定隻能用如下文法:

    doNothing().when(mock.someMethod()) doThrow(new RuntimeException()).when(mock.someMethod()) doNothing().doThrow(new RuntimeException()).when(mock.someMethod())

  • ⑤ 方法的參數可以使用參數模拟器,可以将anyInt()傳入任何參數為int的方法,即anyInt比對任何int類型的參數,anyString()比對任何字元串,anySet()比對任何Set。
  • ⑥ Mock對象隻能調用stubbed方法,調用不了它真實的方法,但是Mockito可以用spy來監控一個真實對象,這樣既可以stubbing這個對象的方法讓它傳回我們的期望值,又可以使得對其他方法調用時将會調用它的真實方法。
  • ⑦ Mockito會自動記錄自己的互動行為,可以用verify(…).methodXxx(…)文法來驗證方法Xxx是否按照預期進行了調用。
    • (1) 驗證調用次數:

      verify(mock,times(n)).someMethod(argument)

      ,n為被調用的次數,如果超過或少于n都算失敗。除了times(n),還有never(),atLease(n),atMost(n)。
    • (2) 驗證逾時:

      verify(mock, timeout(100)).someMethod();

    • (3) 同時驗證:

      verify(mock, timeout(100).times(1)).someMethod();

項目架構過程梳理

1. 0層 整體架構

JUnitCore

main()

方法入口類,所有單元測試用例由這裡開始執行。

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);
    }
    // ignore 
}           

複制

args

是測試類的類名,通過執行

runMain()

方法得到單元測試結果

result

public class Result implements Serializable {
    private static final ObjectStreamField[] serialPersistentFields =
            ObjectStreamClass.lookup(SerializedForm.class).getFields();
    private final AtomicInteger count;
    private final AtomicInteger ignoreCount;
    private final CopyOnWriteArrayList<Failure> failures;
    private final AtomicLong runTime;
    private final AtomicLong startTime;

    // ignore
}           

複制

繼續看一眼

Failure

這個類的構成:

public class Failure implements Serializable {
    private final Description fDescription;
    private final Throwable fThrownException;
    // ignore
}           

複制

閱讀源碼我的做法是:先從頂層開始閉環,再逐漸向下分析,切勿在第一層架構上就深入到第二層第三層等,先閉合每一層再逐漸深入。

在0層階段,我們得到如下結論:傳入測試類的類名數組,經過内部處理後,傳回測試用例執行結果。這些結果包含:執行次數、忽略次數、失敗資訊描述及異常、執行開始時間、執行運作時間。

2. 1層 整體架構

public class JUnitCore {
    Result runMain(JUnitSystem system, String... args) {

        // step 2.1 
        JUnitCommandLineParseResult jUnitCommandLineParseResult = JUnitCommandLineParseResult.parse(args);

        // step 2.2
        RunListener listener = new TextListener(system);
        addListener(listener);

        // step 2.3
        return run(jUnitCommandLineParseResult.createRequest(defaultComputer()));
    }
}           

複制

先看

JUnitCommandLineParseResult

的資料結構,在跟蹤一眼

class JUnitCommandLineParseResult {
    private final List<String> filterSpecs = new ArrayList<String>();
    private final List<Class<?>> classes = new ArrayList<Class<?>>();
    private final List<Throwable> parserErrors = new ArrayList<Throwable>();

    void parseParameters(String[] args) {
        for (String arg : args) {
            try {
                classes.add(Classes.getClass(arg));
            } catch (ClassNotFoundException e) {
                parserErrors.add(new IllegalArgumentException("Could not find class [" + arg + "]", e));
            }
      
    // ignore     
}           

複制

  • classes

    由字元串建構成

    Class<?>

    對象,目的必然是反射。
  • parserErrors

    是上一步建構

    Class<?>

    對象失敗時,存儲異常資訊的容器。
  • filterSpecs

    尚未調用到,先忽略。

至此對所有傳入的

args

校驗和初始化算式完成了。接着初始化了

TextListener

對象并添加到

RunNotifier

中,目的是執行測試用例時候控制台的輸出日志。

前期的準備工作已經做好了,剩下的就是準備真正指令對象,在

JUnit

中它的定義是

org.junit.runner.Request

。最後在調用一下

JUnitCore.run()

方法就完成調用了。

在1層階段,我們看到對

args

的預處理。

JUnit

設計人員使用

org.junit.runner.Request

來作為指令對象(指令模式),

JUnitCore

作為門面類攬下:建立

Request

,排程

Request

,以及生命周期回調管理等一系列髒活。

綜上我們可以推斷出閱讀的重點在:

  • Request

    的構成?支援哪些Request?
  • 如何調用

    Request

    ?調用後

    Result

    是否有再加工?
  • NotifyListener

    生命周期?

2層

Request

的構成?支援哪些Request?

class JUnitCommandLineParseResult {
    public Request createRequest(Computer computer) {
        if (parserErrors.isEmpty()) {
            Request request = Request.classes(
                    computer, classes.toArray(new Class<?>[classes.size()]));
            return applyFilterSpecs(request);
        } else {
            return errorReport(new InitializationError(parserErrors));
        }
    }
    // ignore
}           

複制

異常分支暫不深入看,且看正常情況下的兩步:

  1. Request.classes()

    建構了

    Request

    對象
  2. 調用

    applyFilterSpecs()

    似乎是過濾了某些

    Specs(特征?)

public abstract class Request {

    public static Request classes(Computer computer, Class<?>... classes) {
        try {
            AllDefaultPossibilitiesBuilder builder = new AllDefaultPossibilitiesBuilder(true);
            Runner suite = computer.getSuite(builder, classes);
            return runner(suite);
        } catch (InitializationError e) {
            throw new RuntimeException(
                    "Bug in saff's brain: Suite constructor, called as above, should always complete");
        }
    }

    public static Request runner(final Runner runner) {
        return new Request() {
            @Override
            public Runner getRunner() {
                return runner;
            }
        };
    }
    
}           

複制

千回百轉

computer.getSuite()

最終還是回到了

AllDefaultPossibilitiesBuilder.runnerForClass()

來建構

Runner

對象。

public class AllDefaultPossibilitiesBuilder extends RunnerBuilder {
 
    @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;
    }
    // ignore
}           

複制

each.safeRunnerForClass(testClass);

方法會依據你目前所配置的

@RunWith

注解來選擇實作方法。目前我們使用

@RunWith(Junit4.class)

public class JUnit4Builder extends RunnerBuilder {
    @Override
    public Runner runnerForClass(Class<?> testClass) throws Throwable {
        return new BlockJUnit4ClassRunner(testClass);
    }
}           

複制

劃重點:到此我們得知預設情況下,單元測試最終建立的

Runner

都是

BlockJUnit4ClassRunner

類型,而

Request

又僅是對

Runner

的封裝,是以隻需要精讀

BlockJUnit4ClassRunner

方法即可。

Request

對象已經準備妥當,接着程式執行到

applyFilterSpecs()

方法。

class JUnitCommandLineParseResult {
    private Request applyFilterSpecs(Request request) {
        try {
            for (String filterSpec : filterSpecs) {
                Filter filter = FilterFactories.createFilterFromFilterSpec(
                        request, filterSpec);
                request = request.filterWith(filter);
            }
            return request;
        } catch (FilterNotCreatedException e) {
            return errorReport(e);
        }
    }
    // ignore
}           

複制

啥都不看,憑感覺猜就知道是過濾某些請求(對應注解@Ignore)。但咱們還是務實一點,看看代碼。

public abstract class Request {
    public Request filterWith(Filter filter) {
        return new FilterRequest(this, filter);
    }
    // ignore
}           

複制

這結構熟不熟悉?典型的裝飾器模式——将

Filter

的職責裝飾到原來的

Request

對象上。

public final class FilterRequest extends Request {
    // ignore

    @Override
    public Runner getRunner() {
        try {
            Runner runner = request.getRunner();
            fFilter.apply(runner);
            return runner;
        } catch (NoTestsRemainException e) {
            return new ErrorReportingRunner(Filter.class, new Exception(String
                    .format("No tests found matching %s from %s", fFilter
                            .describe(), request.toString())));
        }
    }
}
public abstract class Filter {
    public void apply(Object child) throws NoTestsRemainException {
        if (!(child instanceof Filterable)) {
            return;
        }
        Filterable filterable = (Filterable) child;
        filterable.filter(this);
    }
    // ignore
}           

複制

至此

Request

對象的構成已經完全透明,在

JUnit

中有如下幾種:

  • SortingRequest
  • FilterRequest
  • ClassRequest

基于以上的分析,我們知道要實作:

對測試用例進行特定排序,并且過濾掉部分用例的需求

是非常容易實作的 —— 裝飾器。

2層 如何調用

Request

?調用後

Result

是否有再加工?

public class JUnitCore {
    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;
    }
    // ignore
}           

複制

執行

runner.run(notifier);

的前後環繞

notifier

通知,執行完

removeListener

避免記憶體洩露。生命周期回調這塊太直接,直接略過。跟一下

runner.run(notifier)

看看。

基于上一個段落的分析,我們知道

Runner

的執行個體類型是

BlockJUnit4ClassRunner

,是以直接看它的

run()

方法。

BlockJUnit4ClassRunner

繼承自

ParentRunner

:

public abstract class ParentRunner<T> extends Runner implements Filterable,
        Sortable {
    
    protected Statement childrenInvoker(final RunNotifier notifier) {
        return new Statement() {
            @Override
            public void evaluate() {
                runChildren(notifier);
            }
        };
    }
    
    protected Statement classBlock(final RunNotifier notifier) {
        Statement statement = childrenInvoker(notifier);
        if (!areAllChildrenIgnored()) {
            statement = withBeforeClasses(statement);
            statement = withAfterClasses(statement);
            statement = withClassRules(statement);
        }
        return statement;
    }
        
    @Override
    public void run(final RunNotifier notifier) {
        EachTestNotifier testNotifier = new EachTestNotifier(notifier,
                getDescription());
        try {
            Statement statement = classBlock(notifier);
            statement.evaluate();
        } catch (AssumptionViolatedException e) {
            testNotifier.addFailedAssumption(e);
        } catch (StoppedByUserException e) {
            throw e;
        } catch (Throwable e) {
            testNotifier.addFailure(e);
        }
    }
    // ignore
}           

複制

這個方法的傳回類型是

void

,并且例外了兩種異常:

  • AssumptionViolatedException

    表明假設不成立,無任何異常抛出。
  • StoppedByUserException

    使用者主動停止單元測試,單獨抛出異常。
  • 其餘情況下都由

    testNotifier

    接口異常。

最最最最重要的部分就是與

Statement

相關聯的部分,這部分是單元測試的核心功能。

classBlock

方法做的事情:将測試類中的測試用例映射成

Statement

對象,并按照

@Before

>

@Test

>

@After

的順序建構職責鍊。

建構完成後調用

statement.evaluate()

,這是最後的掙紮調用了。所有的

evaluate()

都會進到方法:

// ParentRunner.class

    private volatile RunnerScheduler scheduler = new RunnerScheduler() {
        public void schedule(Runnable childStatement) {
            childStatement.run();
        }

        public void finished() {
            // do nothing
        }
    };
    
    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();
        }
    }           

複制

public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> {

    @Override
    protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
        Description description = describeChild(method);
        if (isIgnored(method)) {
            notifier.fireTestIgnored(description);
        } else {
            runLeaf(methodBlock(method), description, notifier);
        }
    }
    
    /**
     * Runs a {@link Statement} that represents a leaf (aka atomic) test.
     */
    protected final void runLeaf(Statement statement, Description description,
            RunNotifier notifier) {
        EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
        eachNotifier.fireTestStarted();
        try {
            statement.evaluate();
        } catch (AssumptionViolatedException e) {
            eachNotifier.addFailedAssumption(e);
        } catch (Throwable e) {
            eachNotifier.addFailure(e);
        } finally {
            eachNotifier.fireTestFinished();
        }
    }
    
    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;
    }
    
}           

複制

執行

evaluate()

調用是整個

JUnit

中最簡單的事情了,複雜性展現在建構

Statement

的職責鍊上,比如:前面對基本

@Test

的用例的建構,到現在在

methodBlock()

中追加

@Timeout @ExpectingException

相應的處理。

結束語

單元測試不是來惡心開發者的,它是幫助開發者盡早發現問題的利器。因為問題越往後發現,它的修複成本就會越高。

GitHub上絕大多數優秀的項目單元測試的覆寫率都是90%以上,在這些項目(前端、後端、用戶端)裡面,我們可以從中學到豐富的測試技巧。是以,不能說不知道怎麼寫單元測試噢~