[Java] [Unit Test] Mocking Framework - JMockit

轉(zhuǎn)到Java之后我還沒有系統(tǒng)地學(xué)習(xí)那些常用的mock框架,平時(shí)寫代碼都是模仿著別人的code東抄抄西抄抄,不會(huì)的再去stack overflow找找答案。最近發(fā)現(xiàn)很多新人都是這樣寫單元測(cè)試的,然后看看代碼庫(kù),同一個(gè)包里用不同的框架的大有人在。我并不是說這樣不好,但是這樣很容易誤導(dǎo)新人。我遇見過兩次有人在寫單元測(cè)試的時(shí)候,annotation用的是一個(gè)mock框架,在setup的時(shí)候又是用的另一個(gè)框架,然后糾結(jié)著怎么跑不通,各種錯(cuò)誤呢!???♀?我自己摸爬滾打了一陣之后,發(fā)現(xiàn)寫單元測(cè)試的時(shí)候最好的參考文檔并不是別人的代碼,而是官方的Tutorial。
于是我開了這個(gè)系列,打算講講大家常用的JMockit和Mockito等框架,也借此機(jī)會(huì)系統(tǒng)學(xué)習(xí)一遍。這里假設(shè)讀者對(duì)單元測(cè)試的基本知識(shí)、AAA (Arrange, Act, Assert)的三段式以及什么是mock都了解,而且這里的mock框架與你所用的單元測(cè)試的框架Junit/TestNG沒有關(guān)系。
對(duì)于每個(gè)框架,我會(huì)先介紹它的主要的annotation和功能;然后以自己經(jīng)常寫到的use cases舉例說明用這個(gè)框架該怎么寫。


JMockit

JMockit能mock什么?

The type of the mock field or parameter can be any kind of reference type: an interface, a class (including abstract and final ones), an annotation, or an enum.

總的來說JMockit非常強(qiáng)大,只要你想要mock的,它幾乎都能做到。雖然在StackOverflow上Mockito被評(píng)為“the best mock framework for Java”,但是仍然有很多人更欣賞JMockit (Comparison between Mockito vs JMockit)。可以看到在Slant上也有Mockito和JMockit的比較,那里非常清楚地列出了JMockit的各種優(yōu)點(diǎn),而它的唯一缺點(diǎn)是上手慢(Learning curve is a bit steep)而已。在寫這篇文章之前,我也是首先Mockito的,但是整個(gè)文檔看下來,我開始有點(diǎn)兒喜歡JMockit了。
// 除了Mockito的when語句比JMockit的Expectations好看,我想不到別的理由拒絕JMockit了:)

Annotations

  • @Tested:用來修飾被測(cè)試的類的對(duì)象,這個(gè)對(duì)象會(huì)自動(dòng)被實(shí)例化,并且會(huì)對(duì)被@Injectable修飾的字段進(jìn)行構(gòu)造器注入。
  • @Injectable: which constrains mocking to the instance methods of a single mocked instance.
    一種用于聲明mock字段或者參數(shù)的方法,用來修飾被測(cè)試類中的一些dependency字段,@Injectable修飾的字段會(huì)自動(dòng)注入到@Tested對(duì)應(yīng)的實(shí)例中,如果沒初始賦值,那么JMockit會(huì)以相應(yīng)規(guī)則進(jìn)行初始化。
  • @Mocked: which will mock all methods and constructors on all existing and future instances of a mocked class (for the duration of the tests using it).
    一種用于聲明mock字段或者參數(shù)的方法,與@Injectable不同的是,被它修飾的類實(shí)例(所有實(shí)例,包括將來創(chuàng)建的)的任何行為(所有的方法和構(gòu)造函數(shù))都會(huì)被mock掉。
  • @Capturing: which extends mocking to the classes implementing a mocked interface, or the subclasses extending a mocked class.
    一種用于聲明mock字段或者參數(shù)的方法,使用它修飾的實(shí)例可以把mock擴(kuò)展到被mocked接口的實(shí)現(xiàn),或者被mocked類的子類。

三段式

JMockit中也有一個(gè)三段式模型,Record-Replay-Verify model。
Record:準(zhǔn)備階段,先記錄下我們期望的即將在測(cè)試中被調(diào)用的方法的mock行為。
Replay:執(zhí)行階段,我們的測(cè)試代碼被執(zhí)行,之前準(zhǔn)備階段的記錄被重放。
Verify:驗(yàn)證階段,驗(yàn)證我們期望的調(diào)用都預(yù)期發(fā)生。

Use Cases

話不多說,先貼一段Demo代碼,把自己平時(shí)用得比較多的需要mock的情況集中起來。(其中一些跟單元測(cè)試無關(guān)的其他代碼沒有貼出來)

public class DemoService {
    private DependencyY dependencyY;
    private DependencyYY dependencyYY;
    private DependencyZ dependencyZ;

    public DemoService(DependencyZ dependencyZ) {
        this.dependencyZ = dependencyZ;
    }

    public int run() {
        DependencyX dependencyX = new DependencyX("inputOfX");
        String x = dependencyX.getX();
        List<String> y = dependencyY.getY("default");
        List<String> list = new ArrayList<>();

        for (String s : y) {
            try {
                list.add(dependencyYY.doSomethingForY(s));
            } catch (DemoException ex) {
                // Handle the exception
            }
        }

        int index = doSomething(list, x);
        if (StaticDependency.isEnabled() && -1 == index) {
            dependencyZ.sendNotification("ErrorMessage");
        }

        StaticDependency.doAnything();
        return index;
    }
}

下面是用JMockit寫的測(cè)試代碼(環(huán)境:Java 8 and JUnit 4)

@RunWith(JMockit.class)
public class DemoServiceTest {
    @Tested
    private DemoService demoService;
    @Mocked
    private DependencyX dependencyX;
    @Injectable
    private DependencyY dependencyY;
    @Injectable
    private DependencyYY dependencyYY;
    @Injectable
    private DependencyZ dependencyZ;

    @Before
    public void setUp() {
        new Expectations(StaticDependency.class) {{
            StaticDependency.isEnabled(); result = true;
        }};

        new MockUp<StaticDependency>() {
            @Mock
            public void doAnything() {
                // Do anything here
            }
        };
    }

    @Test
    public void testDemoService_ShouldGetCorrectIndex_WhenXExistsInY() throws Exception {
        new Expectations() {{
            //new DependencyX(withSuffix("X")); result = dependencyX;
            dependencyX.getX(); result = "str2";
            dependencyY.getY(withAny("")); result = Arrays.asList("B", "A", "C");
            dependencyYY.doSomethingForY(anyString);
            returns("str1", "str2");
            result = new DemoService.DemoException[1];
        }};

        int actualIndex = demoService.run();

        Assert.assertEquals(1, actualIndex);
        new Verifications() {{
            dependencyZ.sendNotification(anyString); times = 0;
        }};
    }

    @Test
    public void testDemoService_ShouldSendNotification_WhenXNotExistsInY() throws Exception {
        new Expectations() {{
            //new DependencyX(withSuffix("X")); result = dependencyX;
            dependencyX.getX(); result = "str4";
            dependencyY.getY(withNotNull()); result = Arrays.asList("B", "A", "C");
            dependencyYY.doSomethingForY((String)any); minTimes = 3;
            returns("str1", "str2", "str3");
        }};

        demoService.run();

        new Verifications() {{
            String message;
            dependencyZ.sendNotification(message = withCapture()); times = 1;
            Assert.assertTrue(message.contains("Error"));
            StaticDependency.doAnything(); times = 1;
        }};
    }
}
mock被測(cè)對(duì)象中的成員有返回值的方法

當(dāng)被測(cè)試對(duì)象中的成員是一個(gè)外部依賴的時(shí)候,以前初始化這個(gè)dependency的方式一般是在構(gòu)造函數(shù)里傳入一個(gè)外部依賴的實(shí)例,現(xiàn)在更多的是采用注入技術(shù),如Spring或Google Guice。這種成員一般也是單例模式的,所以我們?cè)跍y(cè)試的時(shí)候通常用@Injectable來mock這種成員,直接把它注入到我們要測(cè)試的類的實(shí)例中。例如,上面DemoService里面的dependencyY,dependencyYY,dependencyZ。
對(duì)于@Injectable修飾的成員的有返回值的方法,我們一般在測(cè)試的準(zhǔn)備階段(JMockit里的Expectations)會(huì)記下我們期望的這個(gè)方法的返回結(jié)果。
(0)準(zhǔn)備外部依賴的方法的返回值,可以在執(zhí)行測(cè)試代碼之前new Expectations()里寫上我們預(yù)期被調(diào)用的方法,以及對(duì)應(yīng)的result值,下面的X可以是任何類型的值,只要與對(duì)應(yīng)的方法返回值一致即可。

// In test class
@Injectable
private Dependency dependency;

// In test case method
new Expectations() {{
    dependency.method(); result = X;
}};

(1)上面的method()方法沒有參數(shù)值,如果是有參數(shù)列表的方法,則可以在Expectations里面直接寫這個(gè)方法invoke時(shí)的參數(shù)值來精確匹配,也可以用anywith方法進(jìn)行模糊匹配。

new Expectations() {{
    dependency.methodWithParameters(1.0, "blah", Arrays.asList("A", "B")); result = X;
    // or
    dependency.methodWithParameters(anyDouble, anyString, (List<String>)any); result = X;
    // or
    dependency.methodWithParameters(withAny(1.0), withSubstring("bla"), withNotNull()); result = X;
}};

(2)上面的寫法默認(rèn)是需要在測(cè)試執(zhí)行的時(shí)候至少invoke一次,如果多次執(zhí)行到的話,返回結(jié)果都是result里定義的那個(gè)。如果我們希望這個(gè)方法被多次調(diào)用時(shí)返回的結(jié)果不一樣的話,則可以寫多個(gè)result,或者用returns定義多次返回時(shí)的值。例如DemoService里的dependencyYY.doSomethingForY。

new Expectations() {{
    dependency.methodMultipleInvoke(anyString);
    result = X;
    result = Y;
    // or
    dependency.methodMultipleInvoke(anyString);
    returns(X, Y);
}};

(3)當(dāng)上面的方法被調(diào)用非常多次,每次結(jié)果都不一樣,但是跟參數(shù)值相關(guān)時(shí)。我們不能用(2)中的方法列出所有的返回結(jié)果,這個(gè)時(shí)候可以用 delegates

new Expectations() {{
  dependency.method(anyString);
  result = new Delegate() {
    int aDelegateMethod(String s) {
      return s.length();
    }
  };
}};
mock被測(cè)對(duì)象中的成員沒有返回值的方法

從上面的語法我們可以看到,在Expectations中我們是通過定義指定方法的result來記錄被mock的方法的行為的。那么,如果我們用到的方法是沒有返回值的呢?比如,DemoService里面的dependencyZ.sendNotification()。因?yàn)樗鼪]有返回值,我們測(cè)試的對(duì)象的最終表現(xiàn)狀態(tài)與這個(gè)mock的方法具體做了什么沒有關(guān)系,那么我們?cè)趺幢WC它被合理地調(diào)用了呢?答案就是利用Verifications。我們可以在new Verifications()里面定義期望被執(zhí)行的mock方法。

new Verifications() {{
  dependencyZ.sendNotification(withNotNull()); times = 1;
}};

(1)驗(yàn)證一個(gè)不該被執(zhí)行的方法。

new Verifications() {{
  dependency.method(anyString); times = 0;
}};

(2)驗(yàn)證一個(gè)方法被執(zhí)行時(shí)參數(shù)是預(yù)期的,可以在參數(shù)列表里直接用期望值,也可以先用withCapture()拿到該方法invoke時(shí)的參數(shù),然后再進(jìn)行驗(yàn)證。

new Verifications() {{
  dependency.method("ExpectedValue"); times = 1;
  // or
  String actualValue;
  dependency.method(message = withCapture()); times = 1;
  Assert.assertEquals("ExpectedValue", actualValue);
}};

(3)當(dāng)待驗(yàn)證的方法被執(zhí)行時(shí)參數(shù)不確定,但是在一定的范圍內(nèi)時(shí)可以用with方法進(jìn)行模糊匹配。

new Verifications() {{
  dependency.method(withPrefix("Prefix")); times = 1;
}}

(4)值得注意的是,當(dāng)一個(gè)方法在測(cè)試的執(zhí)行過程中被調(diào)用多次的時(shí)候,用上面的withCapture()的話,后一次調(diào)用時(shí)的值會(huì)覆蓋之前的值。所以,如果想拿到會(huì)被多次調(diào)用的mock方法的參數(shù)值,我們需要用withCapture(List)

new Verifications() {{
  List<String> inputs = new ArrayList<>();
  dependency.method(withCapture(inputs)); times = 2;
  Assert.assertEquals(2, inputs.size());
}};

(5)withCapture的另一個(gè)功能是獲取某個(gè)類的實(shí)例化時(shí)的參數(shù)列表,雖然很少用到,但是很簡(jiǎn)單,也順便提一下。

new Verifications() {{
  List<Person> personsInstantiated = withCapture(new Person(anyString, anyInt));
  List<Person> personsCreated = new ArrayList<>();
  dependency.method(withCapture(personsCreated));
}};
mock static方法

對(duì)于有返回值的static方法,可以采用partial mocking的方式直接在Expectations里面像mock其他mocked實(shí)例的方法一樣,直接定義期望的result即可。

new Expectations(StaticDependency.class) {{
  StaticDependency.isEnabled(); result = true;
}};

也可以用MockUp<T>來mock任意的static方法,包括沒有返回值的static方法。

new MockUp<StaticDependency>() {
  @Mock
  public boolean isEnabled() {
    return true;
  }
  @Mock
  public void doAnything() {
    // Do anything here
  }
};

官方文檔:http://jmockit.github.io/tutorial/Mocking.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • JMockit提供了兩套API,一套叫做Expectations,用于基于行為的單元測(cè)試;一套叫做Faking,用...
    孫興斌閱讀 1,981評(píng)論 0 0
  • 名稱解釋 單元測(cè)試(unit testing) 是指對(duì)軟件中的最小可測(cè)試單元進(jìn)行檢查和驗(yàn)證。對(duì)于單元測(cè)試中單元的含...
    GuanYZ閱讀 27,958評(píng)論 1 15
  • 1.Creating mock objects 1.1Class mocks idclassMock=OCMCla...
    奔跑的小小魚閱讀 2,841評(píng)論 0 0
  • 基礎(chǔ)概念 whatJMockit是一款Java類/接口/對(duì)象的Mock工具,目前廣泛應(yīng)用于Java應(yīng)用程序的單元測(cè)...
    visionarywind閱讀 6,063評(píng)論 0 1
  • 模擬類型和實(shí)例 期望 該記錄重放驗(yàn)證模型 經(jīng)常與嚴(yán)格的期望嚴(yán)格和非嚴(yán)格Mock 記錄期望的結(jié)果將調(diào)用與特定實(shí)例匹配...
    歐陽冉冉閱讀 13,749評(píng)論 0 2

友情鏈接更多精彩內(nèi)容