JMockit提供了兩套API,一套叫做Expectations,用于基于行為的單元測試;一套叫做Faking,用于基于狀態(tài)的單元測試。
基于Expectations的單元測試創(chuàng)建mock對象,并且record相應的行為,之后調(diào)用被測代碼(CodeUnderTest),然后進行verify。
Mocking主要考察被測試類與其依賴之間是否正確交互,交互的形式為方法調(diào)用。也就是說,Mocking考察被測試類是否正確調(diào)用了其依賴,正確調(diào)用包括:
- 調(diào)用了那些方法
- 通過怎樣的參數(shù)
- 調(diào)用了多少次
- 調(diào)用的相對順序
關于Mocking和Faking,有兩篇文章對其進行了區(qū)分:
Mocks Aren't Stubs
Stub, Mock and Proxy Testing
2.1 創(chuàng)建并使用mock對象
JMockit可以mock任意class、interface。可以將mock對象聲明為域或者方法的參數(shù)。默認情況下,mock對象的所有 非private 的方法(包括除了object的其他繼承方法)都會被mock,對這些方法的調(diào)用不會執(zhí)行原有代碼,而是會轉(zhuǎn)交給JMockit處理。進行mock風格的測試需要三個步驟:expectation--> 方法調(diào)用 --> verication,示例如下:
@Mocked Dependency mockedDependency
@Test
public void test(final @Mocked AnotherDenpendency anotherDependency) {
new Expectations() {{
mockedDependency.mockedMethod();
result = 1;
}};
codeUnderTest();
new Verifications(){{
anotherDependency.anotherMethod();
times = 1;
}};
}
JMockit會對@Mocked
注解的對象進行依賴注入,所以在Expectation、Verication以及CodeUnderTest中可以直接使用mock對象,不需要手動實例化。
在CodeUnderTest中通過new創(chuàng)建了一個Dependency并調(diào)用了其方法,JMockit會自動將這個方法調(diào)用轉(zhuǎn)移到mock對象上。
public class CodeUnderTest {
public int testMethod() {
Dependency dependency = new Dependency();
return dependency.mockMethod();
}
}
public class Dependency {
public int mockMethod() {
return 1;
}
}
Dependency類的mockMethod方法原本返回值為1,在Expectation中將其返回值設置為2,則在測試過程中該方法將會返回2。
@Mocked
Dependency dependency;
@Test
public void TestMethod() throws Exception {
new NonStrictExpectations() {{
dependency.mockMethod();
result = 2;
}};
CodeUnderTest codeUnderTest = new CodeUnderTest();
assertEquals(2, codeUnderTest.testMethod());
}
2.2 Expectations
Expectations中定義了mock對象將會被調(diào)用的方法以及方法的返回值。Expectations中出現(xiàn)的方法必須被調(diào)用,而調(diào)用的方法不必全部出現(xiàn)在Expectation中。
但是,如果定義了mock對象,并在測試代碼中調(diào)用了它的某個方法,而該方法沒有出現(xiàn)在Expectation中,JMockit并不會執(zhí)行其原有代碼,而是返回null或者原始類型的初始值。例如:
public class CodeUnderTest {
public int testMethod() {
Dependency dependency = new Dependency();
return dependency.mockMethod();
}
}
public class Dependency {
public int mockMethod() {
return 1;
}
}
@RunWith(JMockit.class)
public class MyTest {
@Mocked
Dependency dependency;
@Test
public void TestMethod() throws Exception {
CodeUnderTest codeUnderTest = new CodeUnderTest();
assertEquals(0, codeUnderTest.testMethod());
}
}
2.3 record-replay-verify模型
record : 錄制將要被調(diào)用的方法和返回值
replay:調(diào)用錄制的方法
verify:基于行為的驗證
在record階段實例化Expectations, 在verify階段實例化Verifications。一個測試方法可以包括任意個(包括0)Expectation/Verification。
@Test
public void testMethod(Parameter p) {
//常規(guī)準備代碼
//record
new Expectations(){};
//replay
//調(diào)用測試代碼
//verify
new Verifications(){};
//其他驗證代碼
}
2.4 Regular v.s. Strict Expectation
NonStrictExpections
中的方法至少被調(diào)用一次,否則會出現(xiàn)missing invocation
錯誤。之所以說它是常規(guī)的,是因為其中的方法可以調(diào)用多次,也可以顛倒順序,其中沒出現(xiàn)的方法也可以調(diào)用。
StrictExpectations
中方法調(diào)用的次數(shù)和順序都必須嚴格執(zhí)行。同時,如果出現(xiàn)了在StrictExpectations
中沒有聲明的方法,會出現(xiàn)unexpected invocation
錯誤。
- 可以混合使用
StrictExpectations
和NonStrictExpections
,不過一般一個mock對象只出現(xiàn)在其中之一。StrictExpectations
包含了隱式的verification。
2.5 為Expectation錄制結(jié)果
對于返回值非空的函數(shù)(包括構(gòu)造器),可以通過result
設置返回值或拋出異常,該值在replay階段生效。
可以record多個結(jié)果,
mockObject.mockMethod();
result = new Object();
result = new Object();
result = new SomeException();
等價于
mockObject.mockMethod();
returns(new Obejct(), new Object());
result = new SomeException();
Note:
- 返回值可以使用returns()函數(shù),異常值必須要使用result。
- 異常值需要在CodeUnderTest中捕獲,否則無法通過測試。
- 假設在StrictExpectations中錄制了n個結(jié)果,在replay階段并不強制要求調(diào)用n次該函數(shù),調(diào)用1次即可。
2.6 調(diào)用特定對象的方法
通常,聲明了mock對象并在Expectation中進行了record,則在replay階段對該類其他對象的調(diào)用也會返回record的結(jié)果。也就是說,在replay階段,JMockit并不關心調(diào)用的是哪個對象,只要是該mock類的對象就會引用record中的結(jié)果。
示例如下:
@Test
public void TestMethod(@Mocked final Dependency dependency) throws Exception {
new NonStrictExpectations() {{
dependency.intReturnMethod();
returns(1, 2, 3);
}};
Dependency dependency1 = new Dependency();
assertEquals(1, dependency1.intReturnMethod());
Dependency dependency2 = new Dependency();
assertEquals(2, dependency2.intReturnMethod());
Dependency dependency3 = new Dependency();
assertEquals(3, dependency3.intReturnMethod());
}
在大多數(shù)情況下,CodeUnderTest使用mock類的某一個對象,所以是在CodeUnderTest中創(chuàng)建的還是作為參數(shù)傳給它的并不重要。但是,如果CodeUnderTest中包含多個mock對象,而我們需要
- 只mock其中某個對象,其它的并不mock
- 指定調(diào)用某個mock對象的方法
這時,使用 @Injectable 可以mock某個對象。當然,即使用 @Mock 注解mock該類所有對象,也有其他方法限制Expectation中的匹配。
@Injectable
public class Dependency {
public String mockMethod() {
return "realMethod";
}
}
@Test
public void TestMethod(@Injectable final Dependency mockDependency) {
new NonStrictExpectations() {{
mockDependency.mockMethod();
result = "mockMethod";
}};
assertEquals("mockMethod", mockDependency.mockMethod());
assertEquals("realMethod", new Dependency().mockMethod());
}
其他對象不受影響,正常執(zhí)行原有代碼。
note:
- 需要將mock對象傳遞給CodeUnderTest
- static方法和constructor無法被mock
聲明多個mock對象
聲明多個mock對象可以限制和Expectation的匹配。
示例如下:
//參數(shù)中的anotherDependency只是起到占位作用,不會被真正使用
@Test
public void TestMethod(@Mocked final Dependency mockDependency,
@Mocked Dependency anotherDependency) {
new NonStrictExpectations() {{
mockDependency.mockMethod();
result = "mockMethod";
}};
//這個會被mock
assertEquals("mockMethod", mockDependency.mockMethod());
//JMockit仍然攔截了這次調(diào)用,但是由于在Expectation中沒有record,所以返回null
assertNull("realMethod", new Dependency().mockMethod());
}
這種方法看起來比較奇怪,主要用于當CodeUnderTest中包含多個Dependency類的對象,而想要測試其中某個確定對象會被調(diào)用。
mock特定constructor產(chǎn)生的實例
有兩種方式可以實現(xiàn)這個效果,方式1:
@Test
//mockDependency不會被使用
public void TestMethod(@Mocked Dependency mockDependency) {
new NonStrictExpectations() {{
Dependency dependency1 = new Dependency("dependency1");
dependency1.mockMethod(); result="dependency1";
Dependency dependency2 = new Dependency("dependency2");
dependency2.mockMethod(); result="dependency2";
}};
//可以創(chuàng)建多個對象,會匹配到同一個Expectation
assertEquals("dependency1", new Dependency("dependency1").mockMethod());
assertEquals("dependency1", new Dependency("dependency1").mockMethod());
assertEquals("dependency2", new Dependency("dependency2").mockMethod());
//JMockit仍然攔截了這次調(diào)用,但是由于在Expectation中沒有record,所以返回null
assertNull(new Dependency("dependency2").mockMethod());
}
方式2:
@Test
public void TestMethod(@Mocked final Dependency mockDependency1,
@Mocked final Dependency mockDependency2) {
new NonStrictExpectations() {{
new Dependency("dependency1");
result = mockDependency1;
new Dependency("dependency2");
result = mockDependency2;
mockDependency1.mockMethod();
result = "dependency1";
mockDependency2.mockMethod();
result = "dependency2";
}};
assertEquals("dependency1", new Dependency("dependency1").mockMethod());
assertEquals("dependency1", new Dependency("dependency1").mockMethod());
assertEquals("dependency2", new Dependency("dependency2").mockMethod());
assertNull("dependency3", new Dependency("dependency3").mockMethod());
}
兩種方法等效。
2.7 靈活的參數(shù)匹配
在record和verify階段進行方法匹配時,
- 對于原始類型對象,數(shù)值相同即可;
- 對于Object的子類,需要equals()返回true;
- 對于數(shù)組,需要長度相等且每個對象equals()返回true;
除此之外,如果不關心replay時的具體參數(shù),可以使用anyXyz或者withXyz(...)方法。
使用"any"
@Test
public void someTestMethod(@Mocked final DependencyAbc abc)
{
final DataItem item = new DataItem(...);
new Expectations() {{
abc.voidMethod(anyString, (List<?>) any);
}};
new UnitUnderTest().doSomething(item);
new Verifications() {{
abc.anotherVoidMethod(anyLong);
}};
}
- 任何的基本類型都有對應的anyXyz,anyString對應任意字符串。
- any對應任意的對象,在使用時需要進行顯式類型轉(zhuǎn)換: (CastClass) any
- mockit.Invocations類中有可以使用所有anyXyz
- 使用時參數(shù)位置需要一致
使用"with"
any的限制太寬松,with可以選擇特定的子集。
@Test
public void someTestMethod(@Mocked final DependencyAbc abc) {
final DataItem item = new DataItem(...);
new Expectations() {{
abc.voidMethod("str", (List<?>) withNotNull());
abc.stringReturningMethod(withSameInstance(item), withSubstring("xyz"));
}};
new UnitUnderTest().doSomething(item);
new Verifications() {{
abc.anotherVoidMethod(withAny(1L));
}};
}
也可以自定義with方法。
使用"null"
null可以與任何對象匹配,好處是避免類型轉(zhuǎn)換,但是需要有一個any或者with
。
@Test
public void TestMethod(@Mocked final Dependency mock) {
new StrictExpectations() {{
//測試會失敗,因為沒有any或者with
mock.mockMethod(2, null);
//測試通過
mock.mockMethod(anyInt, null);
}};
mock.mockMethod(new Integer(2), "hello world");
}
如何需要的是null,則應該用 withNull() 方法。
varargs
要么使用常規(guī)的參數(shù),要么使用any/with,不能混合使用。
2.8 調(diào)用次數(shù)的限制
在record和verify階段可以使用times
,minTimes
,maxTimes
來限制。
默認為minTimes = 1。
2.9 顯式驗證
對于NonStrictExpectation,可以進行verification。對于StrictExpectation則沒有必要。在new Verifications(){}
中的方法至少被調(diào)用一次。
驗證某個方法沒被調(diào)用
times = 0
驗證順序調(diào)用
普通的new Verifications(){}
沒有驗證其中方法的調(diào)用順序。new VerificationsInOrder(){}
用來驗證(相對)順序。
驗證部分順序
使用unverifiedInvocations()
方法固定不需要驗證的方法的位置。
第一種場景是驗證部分方法的順序,其余方法不需要驗證:
@Test
public void TestMethod(@Mocked final Dependency mock) {
mock.mockMethod1();
mock.mockMethod2();
mock.mockMethod3();
mock.mockMethod4();
new VerificationsInOrder(){
{
// 下面的代碼會失敗:
// Unexpected invocation of: Dependency#mockMethod2()
// 如果兩個方法相連,則其在replay中也必須直接相連
// unverifiedInvocations();
// mock.mockMethod1();
// mock.mockMethod4();
// 成功
mock.mockMethod1();
unverifiedInvocations();
mock.mockMethod4();
}
};
}
第二種場景是關心部分方法順序,另一些方法也需要驗證,但是不關心順序。這時需要兩個Verification塊:
@Test
public void TestMethod(@Mocked final Dependency mock) {
mock.mockMethod1();
mock.mockMethod2();
mock.mockMethod3();
mock.mockMethod4();
new VerificationsInOrder(){{
mock.mockMethod1();
unverifiedInvocations();
mock.mockMethod4();
}};
new Verifications(){{
mock.mockMethod3();
mock.mockMethod2();
}};
}
多個verification塊時,其相對順序會引起比較詭異的事:
@Test
public void TestMethod(@Mocked final Dependency mock) {
mock.mockMethod1();
mock.mockMethod2();
mock.mockMethod3();
mock.mockMethod4();
//下面的代碼會失敗
//MissingInvocation: Missing invocation of:Dependency#mockMethod2()
//顛倒一下兩個verification的順序則會通過
//原因似乎是Verifications會將驗證過的方法刪除
new Verifications(){{
mock.mockMethod3();
mock.mockMethod2();
}};
new VerificationsInOrder(){{
mock.mockMethod1();
mock.mockMethod2();
mock.mockMethod4();
}};
}
full verification
new FullVerifications() {...}
可以保證replay階段調(diào)用的所有方法在verify代碼塊中都有相應的匹配,順序可以不一致。
full verification in order
使用new FullVerificationsInOrder()
限制full verification的目標類型
默認使用full verification時,所有mock類的所有調(diào)用都必須顯式驗證。如果需要限定驗證的類或者實例,使用FullVerifications(xxx.class)
或者FullVerifications(mockObject)
。
驗證沒有調(diào)用發(fā)生
使用空的FullVerifications(xxx.class)
或者FullVerifications(mockObject)
可以驗證在指定類/實例上沒有調(diào)用方法。但是如果Expectation中有minTimes和times的方法會被正常驗證。
2.10 在verification中捕獲調(diào)用參數(shù)
單次調(diào)用捕獲
使用withCapture()
捕獲最后一次調(diào)用的參數(shù)。
@Test
public void capturingArgumentsFromSingleInvocation(@Mocked final Collaborator mock)
{
new Collaborator().doSomething(0.5, new int[2], "test");
new Verifications() {{
double d;
String s;
mock.doSomething(d = withCapture(), null, s = withCapture());
assertTrue(d > 0.0);
assertTrue(s.length() > 1);
}};
}
多次調(diào)用捕獲
使用withCapture(List)
捕獲所有參數(shù)。
@Test
public void capturingArgumentsFromMultipleInvocations(@Mocked final Collaborator mock)
{
mock.doSomething(dataObject1);
mock.doSomething(dataObject2);
new Verifications() {{
List<DataObject> dataObjects = new ArrayList<>();
mock.doSomething(withCapture(dataObjects));
assertEquals(2, dataObjects.size());
DataObject data1 = dataObjects.get(0);
DataObject data2 = dataObjects.get(1);
// Perform arbitrary assertions on data1 and data2.
}};
}
捕獲新實例
使用withCapture(new XX())
@Test
public void capturingNewInstances(@Mocked Person mockedPerson) {
new Person("Paul", 10);
new Person("Mary", 15);
new Person("Joe", 20);
new Verifications() {{
List<Person> personsInstantiated = withCapture(new Person(anyString, anyInt));
}};
}
2.11 使用Delegate在Expectation中定制result
使用場景:在Expectation中需要根據(jù)replay時的參數(shù)值決定返回值。
原理:JMockit攔截調(diào)用,轉(zhuǎn)交給Delegate處理。
@Test
public void delegatingInvocationsToACustomDelegate(@Mocked final DependencyAbc anyAbc){
new Expectations() {{
anyAbc.intReturningMethod(anyInt, null);
result = new Delegate() {
int aDelegateMethod(int i, String s)
{
return i == 1 ? i : s.length();
}
};
}};
// Calls to "intReturningMethod(int, String)" will execute the delegate method above.
new UnitUnderTest().doSomething();
}
- delegate方法的參數(shù)應該與原始方法一致,返回值需要兼容或者為異常。
- 可以delegate構(gòu)造器,這時返回值設置為空。
- delegate參數(shù)中可以有一個 Invocation對象,從而獲得調(diào)用者的引用。
2.12 級聯(lián)mock
出現(xiàn)obj1.getObj2(...).getYetAnotherObj().doSomething(...)
時可能需要mock多個對象。對于一個mock對象:
- Expectation中進行了record,則會返回record的result;
- 如果沒有record,JMockit會自動創(chuàng)建一個返回被注解@Injectable的子對象
public class Dependency {
public CascadeDependency getCascadeDependency() {
//JMockit會攔截這個方法,返回一個非null對象
return null;
}
public CascadeDependency getAnotherCascadeDependency() {
//JMockit會攔截這個方法,返回一個非null對象
return null;
}
public String getString() {
//仍舊返回null
return null;
}
public Object getObject() {
//仍舊返回null
return null;
}
public List<Object> getList() {
//返回empty集合
return null;
}
}
@Test
public void TestMethod(@Mocked Dependency dependency) {
CascadeDependency first = dependency.getCascadeDependency();
CascadeDependency second = dependency.getCascadeDependency();
//調(diào)用另一個方法
CascadeDependency third = dependency.getAnotherCascadeDependency();
//所有都不會為null
assertNotNull(first);
assertNotNull(second);
assertNotNull(third);
//相同方法返回JMockit創(chuàng)建的同一個對象
assertSame(first, second);
//不同方法返回JMockit創(chuàng)建的同一個對象
assertNotSame(first, third);
//String返回null
assertNull(dependency.getString());
//Object返回null
assertNull(dependency.getObject());
//返回empty集合
assertNotNull(dependency.getList());
assertEquals(0, dependency.getList().size());
}
@Test
public void TestMethod(@Mocked Dependency dependency,
@Mocked CascadeDependency cascadeDependency) {
CascadeDependency first = dependency.getCascadeDependency();
CascadeDependency second = dependency.getAnotherCascadeDependency();
//因為子對象也@Mocked,所以會返回同一個對象
assertSame(first, second);
}
JMockit返回的非空對象實際上進行了@Injectable標識,所以:
@Test
public void TestMethod(@Mocked Dependency dependency) {
//雖然CascadeDependency沒有出現(xiàn)在參數(shù)中,
//但是JMockit對其進行了@Injectable
//而由于沒有在Expectation中record mockMethod的result,所以返回空
assertNull(dependency.getCascadeDependency().mockMethod());
//不影響CascadeDependency的其他實例
assertNotNull(new CascadeDependency().mockMethod());
}
也可以在Expectation中使用result指定返回對象,從而禁止JMockit自動生成。
@Test
public void TestMethod(@Mocked final Dependency dependency) {
//在Expectation中指定了返回結(jié)果,因此JMockit不會生成CascadeDependency
new NonStrictExpectations(){{
dependency.getCascadeDependency();
result = null;
result = new CascadeDependency();
}};
//第一次返回null
assertNull(dependency.getCascadeDependency());
//第二次返回新對象
assertNotNull(dependency.getCascadeDependency().mockMethod());
}
mock級聯(lián)調(diào)用特別適合static factory,getCurrentInstance()
永遠不會返回null。
@Test
public void TestMethod(@Mocked final Dependency dependency) {
assertSame(dependency, dependency.getCurrentInstance());
}
在Builder模式中也很方便驗證,
@Test
public void createOSProcessToCopyTempFiles(@Mocked final ProcessBuilder pb) throws Exception{
Process copy = new ProcessBuilder().command(cmdLine).directory(wrkDir).inheritIO().start();
new Verifications() {{ pb.command(withSubstring("copy")).start(); }};
}
2.13 部分mock
有時候只需要mock部分方法,這時候可以用new Expectations(object)
,object可以是實例,也可以是class對象。在replay階段,如果在Expectation中沒有進行record,則會調(diào)用原有代碼。
@Test
public void partiallyMockingASingleInstance() {
final Collaborator collaborator = new Collaborator(2);
new Expectations(collaborator) {{
collaborator.getValue(); result = 123;
// 靜態(tài)方法也可以
Collaborator.doSomething(anyBoolean, "test");
}};
// Mocked:
assertEquals(123, collaborator.getValue());
Collaborator.doSomething(true, "test");
// Not mocked:
assertEquals(45, new Collaborator(45).getValue());
}
- Note:上面的代碼中沒有出現(xiàn)@Mocked注解
沒有record的方法也可以verify,
@Test
public void partiallyMockingA() {
final Collaborator collaborator = new Collaborator(123);
new Expectations(collaborator) {};
int value = collaborator.getValue();
collaborator.simpleOperation(45, "testing", new Date());
// 沒有record也可以verify
new Verifications() {{ c1.simpleOperation(anyInt, anyString, (Date) any); }};
}
另一種實現(xiàn)部分mock的方法:同時標注@Tested和@Mocked。
2.14 mock接口
有些實現(xiàn)類是匿名的:
public interface Service { int doSomething(); }
public final class TestedUnit {
private final Service service = new Service() {
public int doSomething() { return 2; }
};
public int businessOperation() {
return service.doSomething();
}
}
使用@Capturing
標注基類/接口,所有實現(xiàn)類會被mock:
@Capturing Service anyService;
@Test
public void mockingImplementationClassesFromAGivenBaseType() {
new Expectations() {{
anyService.doSomething();
returns(3);
}};
int result = new TestedUnit().businessOperation();
assertEquals(3, result);
}
@Capturing
是@Mock
的增強版,有一個可選參數(shù)maxInstances
用于捕獲前面指定數(shù)量的對象,其默認值為Integer.MAX_VALUE
。
@Test
public void TestMethod(@Capturing(maxInstances = 2) final Dependency dependency1,
@Capturing(maxInstances = 2) final Dependency dependency2,
@Capturing final Dependency remain) {
new NonStrictExpectations() {{
dependency1.getValue();
result = 1;
dependency2.getValue();
result = 2;
remain.getValue();
result = 3;
}};
assertEquals(1, new Dependency().getValue());
assertEquals(1, new Dependency().getValue());
assertEquals(2, new Dependency().getValue());
assertEquals(2, new Dependency().getValue());
assertEquals(3, new Dependency().getValue());
}
上面的@Capturing
是出現(xiàn)在參數(shù)列表中的,如果是作為field聲明的,maxInstances
會失效,@Capturing
退化為@Mock
。
2.15 自動注入被測試類
用@Tested
標注被測試類,在運行測試方法時,如果該實例仍然為null,JMockit會自動組裝相關mock對象,進行初始化。在組裝被測試類過程中,相關mock對象必須使用@Injectable標記,非mock對象除了使用@Injectable標記,還需要有明確初始值。
public class SomeTest {
@Tested CodeUnderTest tested;
@Injectable Dependency dep1;
@Injectable AnotherDependency dep2;
@Injectable int someIntegralProperty = 123;
@Test
public void someTestMethod(@Injectable("true") boolean flag) {
tested.exerciseCodeUnderTest();
}
}
注入先根據(jù)類型匹配,再根據(jù)參數(shù)名稱匹配。