JMockit使用總結


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技術


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM