轉(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ù)值來精確匹配,也可以用any或with方法進(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
}
};