使用JMockit編寫java單元測試

之前《有效使用Mock編寫java單元測試》一文中層介紹過使用EasyMock和PowerMock來編寫java單元測試,今天介紹一個更加強大的工具——JMockit。

引用單元測試中mock的使用及mock神器jmockit實踐中的java單元測試中各種Mock框架對比,就能明白JMockit有多麼強大:


JMockit是基於JavaSE5中的java.lang.instrument包開發,內部使用ASM庫來動態修改java的字節碼,使得java這種靜態語言可以想動態腳本語言一樣動態設置被Mock對象私有屬性,模擬靜態、私有方法行爲等等,對於手機開發,嵌入式開發等要求代碼儘量簡潔的情況下,或者對於被測試代碼不想做任何修改的前提下,使用JMockit可以輕鬆搞定很多測試場景。

通過如下方式在maven中添加JMockit的相關依賴:

<dependency>
			<groupId>com.googlecode.jmockit</groupId>
			<artifactId>jmockit</artifactId>
			<version>1.5</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.googlecode.jmockit</groupId>
			<artifactId>jmockit-coverage</artifactId>
			<version>0.999.24</version>
			<scope>test</scope>
		</dependency>

JMockit有兩種Mock方式:基於行爲的Mock方式和基於狀態的Mock方式:

引用單元測試中mock的使用及mock神器jmockit實踐中JMockit API和工具如下:


(1).基於行爲的Mock方式:

非常類似與EasyMock和PowerMock的工作原理,基本步驟爲:

1.錄製方法預期行爲。

2.真實調用。

3.驗證錄製的行爲被調用。

通過一個簡單的例子來介紹JMockit的基本流程:

要Mock測試的方法如下:

public class MyObject {
    public String hello(String name){
        return "Hello " + name;
    }
}
使用JMockit編寫的單元測試如下:

@Mocked  //用@Mocked標註的對象,不需要賦值,jmockit自動mock
MyObject obj;

@Test
public void testHello() {
    new NonStrictExpectations() {//錄製預期模擬行爲
        {
            obj.hello("Zhangsan");
            returns("Hello Zhangsan");
            //也可以使用:result = "Hello Zhangsan";
        }
    };
    assertEquals("Hello Zhangsan", obj.hello("Zhangsan"));//調用測試方法
    new Verifications() {//驗證預期Mock行爲被調用
        {
            obj.hello("Hello Zhangsan");
            times = 1;
        }
    };
}
JMockit也可以分類爲非局部模擬與局部模擬,區分在於Expectations塊是否有參數,有參數的是局部模擬,反之是非局部模擬。

而Expectations塊一般由Expectations類和NonStrictExpectations類定義,類似於EasyMock和PowerMock中的Strict Mock和一般性Mock。

用Expectations類定義的,則mock對象在運行時只能按照 Expectations塊中定義的順序依次調用方法,不能多調用也不能少調用,所以可以省略掉Verifications塊;

而用NonStrictExpectations類定義的,則沒有這些限制,所以如果需要驗證,則要添加Verifications塊。

上述的例子使用了非局部模擬,下面我們使用局部模擬來改寫上面的測試,代碼如下:

@Test
public void testHello() {
    final MyObject obj = new MyObject();
    new NonStrictExpectations(obj) {//錄製預期模擬行爲
        {
            obj.hello("Zhangsan");
            returns("Hello Zhangsan");
            //也可以使用:result = "Hello Zhangsan";
        }
    };
    assertEquals("Hello Zhangsan", obj.hello("Zhangsan"));//調用測試方法
    new Verifications() {//驗證預期Mock行爲被調用
        {
            obj.hello("Hello Zhangsan");
            times = 1;
        }
    };
}
模擬靜態方法:

@Test
public void testMockStaticMethod() {
    new NonStrictExpectations(ClassMocked.class) {
        {
            ClassMocked.getDouble(1);//也可以使用參數匹配:ClassMocked.getDouble(anyDouble);
            result = 3;
        }
    };

    assertEquals(3, ClassMocked.getDouble(1));

    new Verifications() {
        {
            ClassMocked.getDouble(1);
            times = 1;
        }
    };
}
模擬私有方法:

如果ClassMocked類中的getTripleString(int)方法指定調用一個私有的multiply3(int)的方法,我們可以使用如下方式來Mock:

@Test
public void testMockPrivateMethod() throws Exception {
    final ClassMocked obj = new ClassMocked();
    new NonStrictExpectations(obj) {
        {
            this.invoke(obj, "multiply3", 1);//如果私有方法是靜態的,可以使用:this.invoke(null, "multiply3")
            result = 4;
        }
    };

    String actual = obj.getTripleString(1);
    assertEquals("4", actual);

    new Verifications() {
        {
            this.invoke(obj, "multiply3", 1);
            times = 1;
        }
    };
}

設置Mock對象私有屬性的值:

我們知道EasyMock和PowerMock的Mock對象是通過JDK/CGLIB動態代理實現的,本質上是類的繼承或者接口的實現,但是在java面向對象編程中,基類對象中的私有屬性是無法被子類繼承的,所以如果被Mock對象的方法中使用到了其自身的私有屬性,並且這些私有屬性沒有提供對象訪問方法,則使用傳統的Mock方法是無法進行測試的,JMockit提供了設置Mocked對象私有屬性值的方法,代碼如下:
被測試代碼:
public class ClassMocked {
    private String name = "name_init";

    public String getName() {
        return name;
    }
    
    private static String className="Class3Mocked_init";
    
    public static String getClassName(){
        return className;
    }
}
使用JMockit設置私有屬性:
@Test
public void testMockPrivateProperty() throws IOException {
    final ClassMocked obj = new ClassMocked();
    new NonStrictExpectations(obj) {
        {
            this.setField(obj, "name", "name has bean change!");
        }
    };

    assertEquals("name has bean change!", obj.getName());
}
使用JMockit設置靜態私有屬性:
@Test
public void testMockPrivateStaticProperty() throws IOException {
    new NonStrictExpectations(Class3Mocked.class) {
        {
            this.setField(ClassMocked.class, "className", "className has bean change!");
        }
    };

    assertEquals("className has bean change!", ClassMocked.getClassName());
}
(2).基於狀態的Mock方式:
JMockit上面的基於行爲Mock方式和傳統的EasyMock和PowerMock流程基本類似,相當於把被模擬的方法當作黑盒來處理,而JMockit的基於狀態的Mock可以直接改寫被模擬方法的內部邏輯,更像是真正意義上的白盒測試,下面通過簡單例子介紹JMockit基於狀態的Mock。
被測試的代碼如下:
public class StateMocked {
    
    public static int getDouble(int i){
        return i*2;
    }
    
    public int getTriple(int i){
        return i*3;
    }
}
改寫普通方法內容:
@Test
public void testMockNormalMethodContent() throws IOException {
    StateMocked obj = new StateMocked();
    new MockUp<StateMocked>() {//使用MockUp修改被測試方法內部邏輯
    	@Mock
      public int getTriple(int i) {
      		return i * 30;
      	}
    };
    assertEquals(30, obj.getTriple(1));
    assertEquals(60, obj.getTriple(2));
    Mockit.tearDownMocks();//注意:在JMockit1.5之後已經沒有Mockit這個類,使用MockUp代替,mockUp和tearDown方法在MockUp類中
}
修改靜態方法的內容:
基於狀態的JMockit改寫靜態/final方法內容和測試普通方法沒有什麼區別,需要注意的是在MockUp中的方法除了不包含static關鍵字以外,其他都和被Mock的方法簽名相同,並且使用@Mock標註,測試代碼如下:
@Test
    public void testGetTriple() {
        new MockUp<StateMocked>() {
            @Mock  
            public int getDouble(int i){  
                return i*20;  
            }
        };  
        assertEquals(20, StateMocked.getDouble(1));  
        assertEquals(40, StateMocked.getDouble(2)); 
    }
JMockit和PowerMock混用時不兼容問題:
由於PowerMock需要在單元測試類上添加@RunWith(PowerMockRunner.class)註解,用於表面使用PowerMock來執行單元測試,而JMockit不需要指定@RunWith註解,因此當一個單元測試類中混合使用PowerMock和JMockit時,JMockit總是會報錯初始化失敗,因此我建議不要在同一個單元測試類中混用PowerMock和JMockit。
另外,JMockit要求必須出現在JVM classpath的中Junit前面位置,因此在添加Maven依賴時記得要把JMockit放在Junit最前面,否則同樣報錯JMockit初始化失敗。

統計JMockit的單元測試覆蓋率:
由於JMockit使用JavaSE5中的java.lang.instrument包開發,因此一般的單元測試覆蓋率統計插件和工具對其無法工作,必須要藉助自帶的JMockit coverage纔行,對於使用Eclemma插件和maven+sonar方式的單元測試覆蓋率統計,分別有如下方法:
(1).Eclemma插件方式:
如果直接使用Eclemma插件來統計單元測試覆蓋率,會發現Eclemma長時間掛起阻塞,強行結束時會報錯說找不到-javaagent等等,解決方法如下:
在Eclipse中右鍵選擇Coverage as ->Coverage Configurations,配置Junit的JVM參數如下:
-javaagent:"${settings.localRepository}"/com/googlecode/jmockit/jmockit-coverage/0.999.24/jmockit-coverage-0.999.24.jar
其中${settings.localRepository}是maven資源目錄,例如:
-javaagent:D:\userdata\administrator\.m2\repository\com\googlecode\jmockit\jmockitcoverage\0.999.24\jmockit-coverage-0.999.24.jar
(2).Maven+Sonar方式:
如果直接使用mvn sonar:sonar命令時,發現任何單元測試無法執行,長時間卡住,強行結束後再次執行時maven工程target目錄下surefire目錄無法刪除,只有重啓機器後才能刪除,解決方法如下:
在pom文件中添加如下的插件:
<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-surefire-plugin</artifactId>
				<version>2.12</version>
				<configuration>
					<argLine>-javaagent:"${settings.localRepository}"/com/googlecode/jmockit/jmockit-coverage/0.999.24/jmockit-coverage-0.999.24.jar</argLine>
				</configuration>
			</plugin>

再次執行mvn sonar:sonar命令就可以正常統計出JMockit的單元測試覆蓋率。

更多內容請參考JMockit官方文檔:http://jmockit.googlecode.com/svn/trunk/www/tutorial.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章