JUnit和Mockito
對於JUnit,這裏就不詳細介紹了,網上的教程有很多,比如這個和這個。
下面主要介紹一下Mockito。
什麼是mock測試,什麼是mock對象?
先來看看下面這個示例:
從上圖可以看出如果我們要對A進行測試,那麼就要先把整個依賴樹構建出來,也就是BCDE的實例。
一種替代方案就是使用mocks
從圖中可以清晰的看出:
- mock對象就是在調試期間用來作爲真實對象的替代品
- mock測試就是在測試過程中,對那些不容易構建的對象用一個虛擬對象來代替測試的方法就叫mock測試
模擬的好處是什麼?
- 提前創建測試; TDD(測試驅動開發)
如果你創建了一個Mock那麼你就可以在service接口創建之前寫Service Tests了,這樣你就能在開發過程中把測試添加到你的自動化測試環境中了。換句話說,模擬使你能夠使用測試驅動開發。
- 團隊可以並行工作
這類似於上面的那點;爲不存在的代碼創建測試。但前面講的是開發人員編寫測試程序,這裏說的是測試團隊來創建。當還沒有任何東西要測的時候測試團隊如何來創建測試呢?模擬並針對模擬測試!這意味着當service藉口需要測試時,實際上QA團隊已經有了一套完整的測試組件;沒有出現一個團隊等待另一個團隊完成的情況。這使得模擬的效益型尤爲突出了。
-
你可以創建一個驗證或者演示程序。
-
爲無法訪問的資源編寫測試
這個好處不屬於實際效益的一種,而是作爲一個必要時的“救生圈”。有沒有遇到這樣的情況?當你想要測試一個service接口,但service需要經過防火牆訪問,防火牆不能爲你打開或者你需要認證才能訪問。遇到這樣情況時,你可以在你能訪問的地方使用MockService替代,這就是一個“救生圈”功能。
-
Mock 可以分發給用戶
-
隔離系統
知道什麼是mock測試後,那麼我們就來認識一下mock框架---Mockito。
Mockito區別於其他模擬框架的地方主要是允許開發者在沒有建立“預期”時驗證被測系統的行爲。
Mockito相關教程:
(與此同時推薦一個東西,SpringOckito, 不過已經2年沒更新了。)
mockito入門實例
Maven依賴:
Xml代碼
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.8.5</version>
<scope>test</scope>
</dependency>
</dependencies>
首先,需要在@Before註解的setUp()中進行初始化(下面這個是個測試類的基類)
Java代碼
public abstract class MockitoBasedTest {
@Before
public void setUp() throws Exception {
// 初始化測試用例類中由Mockito的註解標註的所有模擬對象
MockitoAnnotations.initMocks(this);
}
}
Java代碼
import static org.mockito.Mockito.*;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
public class SimpleTest {
@Test
public void simpleTest(){
//創建mock對象,參數可以是類,也可以是接口
List<String> list = mock(List.class);
//設置方法的預期返回值
when(list.get(0)).thenReturn("helloworld");
String result = list.get(0);
//驗證方法調用(是否調用了get(0))
verify(list).get(0);
//junit測試
Assert.assertEquals("helloworld", result);
}
}
創建mock對象不能對final,Anonymous ,primitive類進行mock。
可對方法設定返回異常
Java代碼
when(list.get(1)).thenThrow(new RuntimeException("test excpetion"));
stubbing另一種語法(設置預期值的方法),可讀性不如前者
Java代碼
doReturn("secondhello").when(list).get(1);
沒有返回值的void方法與其設定(支持迭代風格,第一次調用donothing,第二次dothrow拋出runtime異常)
Java代碼
doNothing().doThrow(new RuntimeException("void exception")).when(list).clear();
list.clear();
list.clear();
verify(list,times(2)).clear();
參數匹配器(Argument Matcher)
Matchers類內加你有很多參數匹配器 anyInt、anyString、anyMap.....Mockito類繼承於Matchers,Stubbing時使用內建參數匹配器,下例:
Java代碼
@Test
public void argumentMatcherTest(){
List<String> list = mock(List.class);
when(list.get(anyInt())).thenReturn("hello","world");
String result = list.get(0)+list.get(1);
verify(list,times(2)).get(anyInt());
Assert.assertEquals("helloworld", result);
}
需要注意的是:如果使用參數匹配器,那麼所有的參數都要使用參數匹配器,不管是stubbing還是verify的時候都一樣。
EclEmma
在衆多的Java覆蓋率測試工具中,開源的Emma是最著名的一個,而EclEmma相當於是它在Eclipse上的圖形化界面插件。它使用簡單,結果直觀。
安裝很簡單,打開Eclipse,點擊Help → Install New Software →輸入update.eclemma.org,安裝軟件即可。
首先,我們可以建立一個HelloWorld類,然後通過Coverage來運行它。
執行完畢之後,我們正在編輯 HelloWorld.java 的窗口將會變成如下所示:
EclEmma 用不同的色彩標示了源代碼的測試情況。其中,綠色的行表示該行代碼被完整的執行,紅色部分表示該行代碼根本沒有被執行,而黃色的行表明該行代碼部分被執行。黃色的行通常出現在單行代碼包含分支的情況。
EclEmma 還提供了一個單獨的視圖來統計程序的覆蓋測試率。可以選擇行覆蓋(Line),分支(Branch)覆蓋等多種覆蓋率檢測標準。
Spring與單元測試
首先在maven中加載以下的庫,尤其是第三個。
[庫] junit:4.12
[庫] mockito-core:1.10.19
[庫] spring-boot-starter-test:1.3.1
Pom.xml (節選)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
接下來,如果你要用到一些Spring自帶的註解,比如@Autowired
的話,最好是在測試類的基類中,加入如下註解,這樣會使得測試時先將SpringBoot運行起來。
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@SpringApplicationConfiguration(classes = Application.class)
接下來需要在@Before註解的setUp()中進行初始化
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@SpringApplicationConfiguration(classes = Application.class)
public abstract class MockitoBasedTest {
@Before
public void setUp() throws Exception {
// 初始化測試用例類中由Mockito的註解標註的所有模擬對象
MockitoAnnotations.initMocks(this);
}
}
由於Eclipse對於import static
的支持很差,你可能還需要記得加上
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
接下來我們爲每個類創建測試用例,在比如Service的一個類上面右鍵-新建-JUnit Test Case,注意要把測試類的目錄改到src/test/java
。
至於其他的部分,與上文中提到的mockito的測試步驟基本相同。至於涉及到@Autowired這種,涉及Spring框架的註解而導致測試無法順利進行的問題,請看下一節的講解。
相關鏈接:
測試中遇到的問題和解決辦法
1) 被測試類、測試類的覆蓋率不同
我們以WeChatServiceImpl
類和它的測試類爲例,WeChatServiceImpl的代碼不變,爲了達到上圖的效果,我們把測試函數中內容刪掉:
@Test
public void testIsOutOfTime() {
;
}
因此可以看出,
-
100%是代表測試函數的每一行都成功執行了,比如我只輸入一個分號;
-
但是,4.2%才代表的是被測函數的實際覆蓋率。
2) 被測類中@Autowired註解,如何控制其中Repository返回值
public class GameHelper {
@Autowired
private PointRepository pointRepository;
public boolean checkLineItem(final Line line) {
Point fromPoint = pointRepository.findById(line.getFromPointId()); //如何控制這個repository的返回?
Point toPoint = pointRepository.findById(line.getToPointId());
return fromPoint.getID().equals(toPoint.getID());//簡化了原函數
}
...
}
因爲在目前的單元測試中,Spring一個很特殊的註解是@Autowired
。(@Autowired
可以對成員變量、方法和構造函數進行標註,來完成自動裝配的工作。)
如果我們要寫個testCheckLineItem()函數的話,我們怎麼控制fromPoint和toPoint呢?
因爲不能改變被測類,因此我曾經嘗試在測試類中使用過以下方法:
-
在測試函數中,使用new PointRepository().tostory
-
對pointRepository使用@Mock,@Spy,@InjectMocks等
-
對pointRepository加@Autowired註解,然後發現註解無效。於是在所有測試類的基類中,增加如下註解以啓用@Autowired,但是依然有問題。(不過如果想使用@Autowired一類的註解,下面這個代碼是必須加的。)
@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @SpringApplicationConfiguration(classes = Application.class)
-
後來與@Autowired一起加了@Spy也不行!!
-
以及對@Mock,@Spy,@InjectMocks的各種使用組合...
最後,經過朋友的提示以及查找,我查到了這個問題的英文解釋和中文的解答。
正確答案是:對被測類中@Autowired
的對象,用@Mocks
標註;對被測類自己,用@InjectMocks
標註。代碼如下:
public class GameHelperTest {
@Mock
private PointRepository pointRepository;
@InjectMocks
private GameHelper gamehelper; //pointRepository作爲mock對象被注入到gamehelper中,gamehelper其他成員變量不變
public void testCheckLineItem() {
Line line = new Line(***);
when(pointRepository.findById(123L)).thenReturn(new Point(***));
when(pointRepository.findById(456L)).thenReturn(new Point(***));
assertTrue(gamehelper.checkLineItem(line));
}
...
}
至於原因,我們回到mockito的官方文檔中去看關於@InjectMocks的解釋。
@InjectMocks - injects mock or spy fields into tested object automatically.
換言之,被@Mock
標註的對象會自動注入到被@InjectMocks
標註的對象中。比如在本例中,GameHelper中的成員變量pointRepository(的函數),就會被我們用在測試用例中改寫過返回值的pointRepository對象替換掉。
另外,經測試,thenReturn返回的是對象引用而不是深複製了對象本身(所以可以減少寫thenReturn()的次數)。
3) 被測函數調用被測類其他函數,怎麼控制返回值?
比如在CreateGameServiceImpl
這個類中,有這樣一段函數
public class CreateGameServiceImpl implements CreateGameService {
...//省略成員變量
public FullGame createGame(String name, Long creatorId, List<Point> points, List<Selection> selections, List<Line> lines) {
Game gameItem = createBlankGame(name, creatorId); //createBlankGame()爲CreateGameServiceImpl中另一個函數
那麼,如果我還沒實現createBlackGame(),我在測試函數裏應該怎麼控制它呢?這次用2)中的方法@Mock + @InjectMocks
就不行了,因爲他們屬於同一個類。
(這個問題@Xander 覺得應該實現了被調用的函數纔好,但是既然mock的存在很多時候是爲了在函數都沒實現的情況下編寫測試,因此我覺得繼續研究。)
後來自己通過查閱**官方的文檔,解決辦法**是使用spy()
命令,結合doReturn()
:
public class CreateGameServiceImplTest {
//這部分不需要改。省略其他成員變量
@Mock
private GameHelper gameHelper;
@InjectMocks
CreateGameServiceImpl serviceimpl;
@Test
public void testCreateGameStringLongListOfPointListOfSelectionListOfLine() {
serviceimpl = spy(serviceimpl); //將serviceimpl部分mock化
doReturn(***).when(serviceimpl).createBlankGame(a, b); //這裏必須用doReturn()而不能是when().thenReturn()
...
}
}
原因我們在最後解釋。
首先我們來看文檔中對於Spy()的解釋:
You can create spies of real objects. When you use the spy then the methods are called (unless a method was stubbed).
Spying on real objects can be associated with "partial mocking" concept.(重點是,spy與"部分mock"相關。)
對於Spy,官方有個Sample:
List list = new LinkedList();
List spy = spy(list);
//optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);
//using the spy calls real methods
spy.add("one");
spy.add("two");
//prints "one" - 這個函數還是真實的
System.out.println(spy.get(0));
//100 is printed - size()函數被替換了
System.out.println(spy.size());
通俗來講,在我個人理解,Spy()可以使一個對象的一部分方法被用戶替換。
在我們的例子中,CreateGameServiceImpl
中的函數createGame()
調用了createBlankGame()
,而後者可能是未實現的。
但是此時CreateGameServiceImpl
類的註解是@InjectMocks
而不是@Mock
,只能接收@Mock對象的注入,而自己的方法無法被mock(stub)。
因此我們通過spy()
,將CreateGameServiceImpl部分mock化,從而將createBlankGame()函數替換掉。
不過這裏如果遇到private的被調函數就沒辦法了。
覆蓋率
對於單元測試,一個重要的衡量指標就是覆蓋率。
覆蓋率分爲:
行覆蓋(Line Coverage,又叫段覆蓋/語句覆蓋(Statement Coverage)等)
分支覆蓋(Branch Coverage,又叫判定覆蓋Decision Coverage等)
條件覆蓋(Condition Coverage)
路徑覆蓋(Path Coverage)等等
據瞭解,所有這些覆蓋中行覆蓋(Line coverage)是最簡單的,也是最常用的、最有效的覆蓋率。
在EclEmma中可以選擇任意一種覆蓋率,以下是我的項目中行覆蓋率的截圖。
3. 指令覆蓋(Instruction Coverage),方法覆蓋(Method Coverage):均爲100%,就不截圖了。
*額外工作:實現任意兩個對象比較
由於單元測試常常需要用到assertEquals()
方法,而對於很多自定義的數據結構(比如Point.java)要重寫equals()方法,否則調用equals()只會比較兩個對象的引用,這帶來非常多的麻煩事。
鑑於Web應用中使用的自定義的數據結構(Model)通常是JavaBean規範的(這些類的成員屬性通常是Java的基本數據類型或String,Collection等常見類型),因此我希望通過只對比兩個對象的成員變量的變量類型、變量名、變量的值(如果是集合和數組就深入進去判斷),來判斷兩個對象是否“相等”。
我查了好久,除了發現貌似還真沒人寫,只有apache.commons.beanutils
和apache.commons.collections.comparator
有類似的方法,但是看了他們的源碼覺得跟我要的不是一個東西。
這個工具類,因爲有些細節上寫起來很困難,大概寫了好幾個小時吧,打算以後如果真的沒人做這個,就把它做成一個開源的小工具。
在這裏我同時提供了兩種方式,第一種比較取巧,直接比較兩個對象對應的JSON字符串,這種方法很方便,不容易出錯,但是可能適用範圍上略小一點。
我使用的是JackJson的庫(其他也可以),代碼如下:
static public boolean compareByJson(Object a,Object b){
try {
ObjectMapper objectMapper = new ObjectMapper();
String jsona =objectMapper.writeValueAsString(a);
String jsonb =objectMapper.writeValueAsString(b);
System.out.println(jsona);
System.out.println(jsonb);
return jsona.equals(jsonb);
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
第二種,則是通過Java的反射機制,通過getClass(),getDeclaredFields(),setAccessible(true)
等方法來取得任意對象的成員變量,按順序分析兩者中的兩個變量的變量名、變量類型、變量值是否相等,是否是重寫了equals()方法的常見類型、是否是集合等方面來比較和判斷,也借用了LeetCode上一道難度很低的題的算法,代碼如下(某處仍有bug):
static public boolean compare(Object obj_a, Object obj_b) {
if(obj_a == null && obj_b == null)
return true;
else if (obj_a == null || obj_b == null)
return false;
else {
Field[] fields_a = obj_a.getClass().getDeclaredFields();
Field[] fields_b = obj_b.getClass().getDeclaredFields();
if (fields_a.length != fields_b.length)
return false;
else for (int i = 0; i < fields_a.length; i++) {
fields_a[i].setAccessible(true);
fields_b[i].setAccessible(true);
Object obj_a_innerobj_i = null, obj_b_innerobj_i = null;
try {
obj_a_innerobj_i = fields_a[i].get(obj_a);
obj_b_innerobj_i = fields_b[i].get(obj_b);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
if (fields_a[i].getName() != fields_b[i].getName())
return false;
else if (!fields_a[i].getGenericType().equals(fields_b[i].getGenericType()))
return false;
else if (!SupportedClassesList.contains(obj_a_innerobj_i.getClass()))
if (compare(obj_a_innerobj_i, obj_b_innerobj_i) == false)
return false;
else if (obj_a_innerobj_i instanceof Collection){
//TODO 仍有bug
if(!(((Collection) obj_a_innerobj_i).containsAll((Collection)(obj_b_innerobj_i))&&((Collection)obj_b_innerobj_i).containsAll((Collection)(obj_a_innerobj_i))))
return false;
else if (!obj_a_innerobj_i.equals(obj_b_innerobj_i))
return false;
}
}
return true;
}
相關鏈接: