Jmockit可以做什么
使用JMockit API來mock被依賴的代碼,從而進行隔離測試。
- 類級別整體mock和部分方法重寫
- 實例級別整體mock和部分mock
- mock靜態方法、私有變量、局部方法
- 靈活的參數匹配
maven依賴
Jmockit可以和junit和TestNG配合使用。需要注意的是:
- 如果使用Junit4.5以上,jmockit依賴需要在junit4之前;或者在測試類上添加注解 @RunWith(JMockit.class)。
- 如果是TestNG 6.2+ 或者 JUnit 5+, 沒有位置限制
<!-- 如果使用Junit4.5以上 jmockit依賴需要在junit4之前 -->
<!-- 或者在測試類上添加注解 @RunWith(JMockit.class) -->
<!-- 如果是TestNG 6.2+ 或者 JUnit 5+, 沒有位置限制 -->
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.30</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
版本記錄 版本更新較快。
使用
涉及到三個類:
- 測試類:執行測試代碼的類。
- CUT(Code Under Test):被測試的類,測試此類是否能正確地工作。
- 依賴類:CUT會調用依賴類的方法。
CUT
@Data
public class Person {
private String name;
private Integer age;
private Person friend;
public Person(){}
public Person(String name, Integer age, Person friend){
this.age = age;
this.name = name;
this.friend = friend;
}
@Override
public boolean equals(Object obj) {
return this.name.equals(((Person)obj).getName());
}
}
@Service
public class PersonService {
public String showName(String name){
System.out.println("person show name : " + name);
return name;
}
public int showAge(int age) {
System.out.println("person show age : " + age);
return age;
}
public Person getDefaultPerson(){
return new Person("miao", 3, null);
}
}
@Service
public class CoderService {
@Value("${coder.service.desc}")
private String desc;
public String showWork(String work){
return work;
}
public int showSalary(int salary){
return salary;
}
public String getDesc() {
return desc;
}
public String getPersonName(Person person){
return person.getName();
}
}
基本流程
record(錄制)---- replay(回放) ---- verify(驗證)
record : 設置將要被調用的方法和返回值。
- Expections中的方法至少被調用一次,否則會出現missing invocation錯誤。調用次數和調用順序不限。
- StrictExpectations中方法調用的次數和順序都必須嚴格執行。如果出現了在StrictExpectations中沒有聲明的方法,會出現unexpected invocation錯誤。
replay:調用(未被)錄制的方法,被錄制的方法調用會被JMockit攔截並重定向到record階段設定的行為。
verify:基於行為的驗證,測試CUT是否正確調用了依賴類,包括:調用了哪些方法;通過怎樣的參數;調用了多少次;調用的相對順序(VerificationsInOrder)等。可以使用times,minTimes,maxTimes來驗證。
@Test
public void mockProcessTest(final @Mocked PersonService target){
//錄制預期行為
new Expectations(){
{
target.showName(anyString);
result = "test1";
target.showAge(anyInt);
result = -1;
}
};
//測試代碼
Assert.assertTrue("test1".equals(target.showName("test2")));
Assert.assertTrue(-1 == target.showAge(12));
Assert.assertTrue(-1 == target.showAge(12));
//驗證
new Verifications(){
{
target.showName("test1");
times = 0; //執行了0次。參數一致的才會計數
target.showAge(12);
times = 2; //執行了2次
}
};
}
/**
* Expections中的方法至少被調用一次,否則會出現missing invocation錯誤.
* 調用次數和調用順序不限.
*/
@Test
public void mockExpectationsProcessTest(final @Mocked PersonService service){
new Expectations(){{
service.showAge(anyInt);
result = -1;
}};
//只調用showName會報錯 Missing 1 invocation
service.showName("hahah");
service.showAge(12);
}
/**
* StrictExpectations中方法調用的次數和順序都必須嚴格執行。如果出現了在StrictExpectations中沒有聲明的方法,會出現unexpected invocation錯誤。
* 沒有必要做Verifications驗證。
*/
@Test
public void mockStrictExpectationsProcessTest(final @Mocked PersonService service){
new StrictExpectations(){{
service.showAge(anyInt);
result = -1;
service.showName(anyString);
result = "ok";
}};
//1.下面只執行了一個錄制方法,報錯:unexpected invocation, Missing invocation
// Assert.assertTrue(-1 == service.showAge(12));
//2.下面與錄制順序不一致,會報錯:unexpected invocation, Missing invocation
// Assert.assertTrue("ok".equals(service.showName("test")));
// Assert.assertTrue(-1 == service.showAge(12));
//3.調用沒有錄制的方法,報錯 Unexpected invocation
// service.getDefaultPerson();
//必須全部執行錄制的方法,且順序一致
Assert.assertTrue(-1 == service.showAge(12));
Assert.assertTrue("ok".equals(service.showName("test")));
}
部分注解說明
RunWith(JMockit.class): 指定單元測試的執行類為JMockit.class。
Tested: 指定被測試類,同時mock實例並注入測試類;依賴的類使用Injectable注入。
Injectable: 將對象進行mock並注入測試類。
Mocked:mock一種類型,並注入測試類。
Mocked與Injectable區別:
- Mocked 注入的依賴,類的所有實例都被mock,record的方法,在replay時,按照record的結果返回;沒有record的方法返回默認值。
- Injectable 注入的依賴,只mock指定的實例,record的方法,在replay時,按照record的結果返回;沒有record的方法返回默認值。沒有mock的實例,調用其原始方法。
@RunWith(JMockit.class)
public class MockTest {
//@Mocked 修飾,所有實例都會被mock
@Mocked
private PersonService personService;
// @Injectable 修飾,只mock指定的實例。
@Injectable
private CoderService coderService;
@Test
public void testInstance(){
new Expectations(){
{
personService.showAge(anyInt);
result = -1;
personService.getDefaultPerson();
result = new Person("me", 4, null);
Deencapsulation.invoke(coderService, "showWork", anyString);
result = "java";
}
};
//record的方法,按照給定的結果返回
Assert.assertTrue(-1 == personService.showAge(11));
Assert.assertTrue("java".equals(coderService.showWork("nothing")));
Assert.assertTrue(4 == personService.getDefaultPerson().getAge());
//沒有錄制的方法,返回默認值
Assert.assertTrue(personService.showName("testName") == null);
Assert.assertTrue(coderService.showSalary(100) == 0);
//Mock 所有PersonServiceImpl實例
PersonService pservice = new PersonService();
Assert.assertTrue(-1 == pservice.showAge(11));
Assert.assertTrue(pservice.showName("testName") == null);
//新生成的CoderService實例沒有被mock
CoderService cservice = new CoderService();
Assert.assertTrue("something".equals(cservice.showWork("something")));
Assert.assertTrue(cservice.showSalary(100) == 100);
}
/**
* 可以將參數注入,與類中注入結果一致。
* 但是不要同時在參數中注入,且在測試類中注入,會影響執行結果。
*/
@Test
public void testInjectObj(final @Injectable CoderService coderService){
new Expectations(){
{
coderService.showWork(anyString);
result = "ok";
}
};
Assert.assertTrue("ok".equals(coderService.showWork("hello")));
Assert.assertTrue(coderService.showSalary(100) == 0);
}
使用示例
- 部分mock(實例級別)
在Expectations中傳入被mock實例。 則replay的方法在Expectations中被錄制時,按照record結果返回;沒有被錄制,則調用原有代碼。
與之對應的是Injectable 注入的實例,record的方法,在replay時按照record結果返回;沒有record的方法,返回默認值。
/**
* 部分mock,在Expectations中傳入被mock實例。
* replay的方法在Expectations中被錄制時,按照record結果返回;
* 沒有被錄制,則調用原有代碼
*/
@Test
public void partiallyMock(){
new Expectations(personService){
{
personService.showAge(anyInt);
result = -1;
}
};
//被錄制的方法,按照record結果返回
Assert.assertTrue(-1 == personService.showAge(11));
//未錄制的方法,調用原有代碼
Assert.assertTrue("testName".equals(personService.showName("testName")));
}
- mockUp(類級別)
mockUp的類,被mock的方法,replay的時候都執行mock的方法;沒有被mock的方法,調用原有代碼。
與之對應的事Mocked注入的類,所有record的方法按照record結果返回;沒有record的方法,返回默認值。
/**
* mockUp類,被mock的方法,replay的時候都執行mock的方法;
* 沒有被mock的方法,調用原有代碼
*/
@Test
public void mockUpTest(){
new MockUp<PersonService>(){
@Mock
public String showName(String name){
return "mocked";
}
};
Assert.assertTrue("mocked".equals(new PersonService().showName("test")));
Assert.assertTrue(1 == new PersonService().showAge(1));
}
- mock 靜態方法
@Test
public void testStaticMethod(){
new Expectations(CollectionUtils.class){{
CollectionUtils.isEmpty((Collection<?>) any);
result = true;
}};
List<Integer> list = Lists.newArrayList(1,2,3);
Assert.assertTrue(list.size() == 3);
Assert.assertTrue(CollectionUtils.isEmpty(list));
}
- mock 私有變量
- 局部方法
- mock 參數匹配問題
參數為基本類型時,若mock方法參數設置為anyXXX,則任意此類型參數都可mock成功;若mock方法參數為具體值,則實際參數 equals mock參數時,才能mock成功。
參數為非基本類型時,mock參數不可以為any,執行報錯;若mock參數為具體值,只有傳遞的參數 equals mock參數時,才能mock成功。
public class CoderServiceTest {
@Tested
private CoderService coderService;
@Injectable
private PersonService personService;
@Test
public void testMockCase(){
new Expectations(coderService){{
//mock私有變量
Deencapsulation.setField(coderService, "desc", "coderDesc");
//mock 方法
Deencapsulation.invoke(coderService, "showWork", anyString);
result = "noWork";
}};
//mock 私有變量成功
Assert.assertTrue(coderService.getDesc().equals("coderDesc"));
//mock 私有方法
Assert.assertTrue(coderService.showWork("coder").equals("noWork"));
}
@Test
public void testParamCase(){
new Expectations(coderService){{
//基本類型,mock參數為anyXXX
Deencapsulation.invoke(coderService, "showWork", anyString);
result = "mocked";
//基本類型,mock參數為實際值
Deencapsulation.invoke(coderService, "showSalary", 12);
result = -1;
//非基本類型,mock參數不可以為anyXXX,會報錯 java.lang.IllegalArgumentException: Invalid null value passed as argument 0
// Deencapsulation.invoke(coderService, "getPersonName", (Person)any);
// result = "mocked";
//基本類型,mock參數為實際值
Deencapsulation.invoke(coderService, "getPersonName", new Person("me", 3, null));
result = "mocked";
}};
//基本類型,mock參數為anyXXX, 實際參數為任意值mock成功
Assert.assertTrue(coderService.showWork("java").equals("mocked"));
//基本類型,mock參數為具體值, 實際參數 equals mock參數時,mock成功
Assert.assertTrue(coderService.showSalary(12) == -1);
Assert.assertTrue(coderService.showSalary(100) == 100);
//基本類型,mock參數為實際值,實際參數 equals mock參數時,mock成功
Assert.assertTrue("mocked".equals(coderService.getPersonName(new Person("me", 4, null))));
Assert.assertFalse("mocked".equals(coderService.getPersonName(new Person("you", 3, null))));
}
}
注意事項
- Tested指定的被測試類,必須是實現類,而非接口。否則不能正確實例化,報錯NPE。
參考
JMockit git book 很挫!!!
JMockit Tutorial 很詳細,部分示例已過時,推薦指數四顆星
官網 英文版
JMockit mock 實例使用的是asm技術