一個使用mockito和spring-test的例子
可以在:
https://github.com/weipeng2k/mockito-sample
找到示例。
Java單元測試框架在業界非常多,以JUnit爲事實上的標準,而JUnit只是解決了單元測試的基本骨幹,而對於Mock的支持卻沒有。而同樣,在Mock方面,Java也有很多開源的選擇,諸如JMock、EasyMock和Mockito,而Mockito也同樣爲其中的翹楚,二者能夠很好的完成單元測試的工作。本示例就是介紹如何使用二者來完成單元測試。如果公司自己搞一個單元測試框架,維護將成爲一個大問題,而使用業界成熟的解決方案,將會是一個很好的方式。因爲會有一組非常專業的人替你維護,而且不斷地有新的Feature可以使用,同樣你熟悉這些之後你可以不斷的複用這些知識,而不會由於侷限在某個特定的框架下(其實這些特定的框架也只是封裝了業界的開源方案)。使用JUnit做單元測試的主體框架,如果有Spring的支持,可以使用spring-test進行支持,對於層與層之間的Mock,則使用Mockito來完成。
前言
引言
"I'm not a great programmer; I'm just a good programmer with great habits."
-- Kent Beck
Java單元測試框架在業界非常多,以JUnit爲事實上的標準,而JUnit只是解決了單元測試的基本骨幹,而對於Mock的支持卻沒有。而同樣,在Mock方面,Java也有很多開源的選擇,諸如JMock
、EasyMock
和Mockito
,而Mockito
也同樣爲其中的翹楚,二者能夠很好的完成單元測試的工作。本文就是介紹如何使用二者來完成單元測試。
存在的問題
如果公司自己搞一個單元測試框架,維護將成爲一個大問題,而使用業界成熟的解決方案,將會是一個很好的方式。因爲會有一組非常專業的人替你維護,而且不斷地有新的Feature可以使用,同樣你熟悉這些之後你可以不斷的複用這些知識,而不會由於侷限在某個特定的框架下(其實這些特定的框架也只是封裝了業界的開源方案)。
解決方案
使用JUnit
做單元測試的主體框架,如果有Spring
的支持,可以使用spring-test
進行支持,對於層與層之間的Mock,則使用Mockito
來完成。
使用Mockito進行單元測試
以下例子可以在
mockito-test-case
中找到。
使用Mockito進行mock
先看一下怎樣使用Mockito進行一個對象的Mock,首先添加依賴:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
</dependency>
接下來嘗試對java.util.List
進行Mock,Mock對於List操作的內容進行構造。
構造Mock
先看一下最簡的使用方式。
public void mock_one() {
List<String> list = Mockito.mock(List.class);
Mockito.when(list.get(0)).thenReturn("one");
System.out.println(list.get(0));
Assert.assertEquals("one", list.get(0));
}
上面代碼中Mockito.mock
可以構造一個Mock對象,這個對象沒有任何作用,如果調用它的方法,如果有返回值的話,它會返回null。這個時候可以向其中加入mock邏輯,比如:Mockito.when(xxx.somemethod()).thenReturn(xxx)
,這段邏輯就會在當有外界調用xxx.somemethod()
時,返回那個在thenReturn中的對象。
構造一個複雜的Mock
有時我們需要針對輸入來構造Mock的輸出,簡單的when和thenReturn無法支持,這時就需要較爲複雜的Answer
。
@Test(expected = RuntimeException.class)
public void mock_answer() {
List<String> list = Mockito.mock(List.class);
Mockito.when(list.get(Mockito.anyInt())).thenAnswer(
invocation -> {
Object[] args = invocation.getArguments();
int index = Integer.parseInt(args[0].toString());
// int index = (int) args[0];
if (index == 0) {
return "0";
} else if (index == 1) {
return "1";
} else if (index == 2) {
throw new RuntimeException();
} else {
return String.valueOf(index);
}
});
Assert.assertEquals("0", list.get(0));
Assert.assertEquals("1", list.get(1));
list.get(2);
}
有時候需要構造複雜的返回邏輯,比如參數爲1的時候,返回一個值,爲2的時候,返回另一個值。那麼when和thenAnswer就可以滿足要求。
上面代碼可以看到當對於List的任意的輸入Mockito.anyInt()
,會進行Answer
回調的處理,任何針對List的輸入都會經過它的處理。這可以讓我完成更加柔性和定製化的Mock操作。
斷言選擇
當然我們可以使用System.out.println來完成目測,但是有時候需要讓JUnit插件或者maven的surefire插件能夠捕獲住測試的失敗,這個時候就需要使用斷言了。我們使用org.junit.Assert來完成斷言的判斷,可以看到通過簡單的assertEquals就可以了,當然該類提供了一系列的assertXxx來完成斷言。
使用IDEA在進行斷言判斷時非常簡單,比Eclipse要好很多,比如:針對一個int x
判斷它等於0,就可以直接寫x == 0
,然後代碼提示生成斷言。
真實案例
下面我們看一個較爲真實的例子,比如:我們有個MemberService
用來insertMember。
public interface MemberService {
/**
* <pre>
* 插入一個會員,返回會員的主鍵
* 如果有重複,則會拋出異常
* </pre>
*
* @param name name不能超過32個字符,不能爲空
* @param password password不能全部是數字,長度不能低於6,不超過16
* @return PK
*/
Long insertMember(String name, String password) throws IllegalArgumentException;
}
其對應的實現。
public class MemberServiceImpl implements MemberService {
private UserDAO userDAO;
@Override
public Long insertMember(String name, String password)
throws IllegalArgumentException {
if (name == null || password == null) {
throw new IllegalArgumentException();
}
if (name.length() > 32 || password.length() < 6
|| password.length() > 16) {
throw new IllegalArgumentException();
}
boolean pass = false;
for (Character c : password.toCharArray()) {
if (!Character.isDigit(c)) {
pass = true;
break;
}
}
if (!pass) {
throw new IllegalArgumentException();
}
Member member = userDAO.findMember(name);
if (member != null) {
throw new IllegalArgumentException("duplicate member.");
}
member = new Member();
member.setName(name);
member.setPassword(password);
Long id = userDAO.insertMember(member);
return id;
}
public void setUserDAO(UserDAO userDAO) {
this.userDAO = userDAO;
}
}
可以看到實現通過聚合了userDAO,來完成操作,而業務層的代碼的單元測試代碼,就必須隔離UserDAO,也就是說要Mock這個UserDAO。
下面我們就使用Mockito來完成Mock操作。
public class MemberWithoutSpringTest {
private MemberService memberService = new MemberServiceImpl();
@Before
public void mockUserDAO() {
UserDAO userDAO = Mockito.mock(UserDAO.class);
Member member = new Member();
member.setName("weipeng");
member.setPassword("123456abcd");
Mockito.when(userDAO.findMember("weipeng")).thenReturn(member);
Mockito.when(userDAO.insertMember((Member) Mockito.any())).thenReturn(
System.currentTimeMillis());
((MemberServiceImpl) memberService).setUserDAO(userDAO);
}
@Test(expected = IllegalArgumentException.class)
public void insert_member_error() {
memberService.insertMember(null, "123");
memberService.insertMember(null, null);
}
@Test(expected = IllegalArgumentException.class)
public void insert_exist_member() {
memberService.insertMember("weipeng", "1234abc");
}
@Test(expected = IllegalArgumentException.class)
public void insert_illegal_argument() {
memberService
.insertMember(
"akdjflajsdlfjaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfsadfasdfasf",
"abcdcsfa123");
}
@Test
public void insert_member() {
System.out.println(memberService.insertMember("windowsxp", "abc123"));
Assert.assertNotNull(memberService.insertMember("windowsxp", "abc123"));
}
}
可以看到,在測試開始的時候,利用了Before來完成Mock對象的構建,也就是說在test執行之前完成了Mock對象的初始化工作。
但仔細看上述代碼中,MemberService
的實現MemberServiceImpl
是直接構造出來的,它依賴了實現,但是我們的測試最好不要依賴實現進行測試的。同時UserDAO
也是硬塞給MemberService
的實現,這是因爲我們常用Spring來裝配類之間的關係,而單元測試沒有Spring的支持,這就使得測試代碼需要硬編碼的方式來進行組裝。
那麼我們如何避免這樣的強依賴和組裝代碼的出現呢?結論就是使用spring-test來完成。
使用Spring-Test來進行單元測試
以下例子可以在
classic-spring-test
中找到。
spring-test是springframework中一個模塊,主要也是由spring作者Juergen Hoeller
來完成的,它可以方便的測試基於spring的代碼。
引入spring-test
spring-test
只需要引入依賴就可以完成測試,非常簡單。它能夠幫助我們啓動一個測試的spring容器,完成屬性的裝配,但是它如何同Mockito
集成起來是一個問題,我們採用配置的方式進行。
加入依賴
增加依賴:
該版本一般和你使用的spring版本一致
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
配置
由於Mockito
支持mock
方法構造,所以我們可以將它通過spring factory bean的形式融入到 spring 的體系中。我們針對MemberService
進行測試,需要對UserDAO
進行Mock,我們只需要在配置中配置即可。
配置在MemberService.xml中,這裏需要說明一下 沒有使用共用的配置文件, 目的就是讓大家在測試的時候能夠相互獨立,而且在一個配置文件中配置的Bean越多,就證明你要測試的類依賴越複雜,也就是越不合理,逼迫自己做重構。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"
default-autowire="byName">
<bean id="memberService" class="com.murdock.tools.mockito.service.MemberServiceImpl"/>
<bean id="userDAO" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg>
<value>com.murdock.tools.mockito.dao.UserDAO</value>
</constructor-arg>
</bean>
</beans>
在進行spring測試之前,我們必須有一個spring的配置文件,用來構造applicationContext
,注意上面紅色的部分,這個UserDAO
就是MemberServiceImpl
需要的,而它利用了spring的FactoryBean
方式,通過mock工廠方法完成了Mock對象的構造,其中的構造函數表明了這個Mock是什麼類型的。只用在配置文件中聲明一下就可以了。
構造Mock
先看一下使用spring-test如何寫單元測試:
@ContextConfiguration(locations = {"classpath:MemberService.xml"})
public class MemberSpringTest extends AbstractJUnit4SpringContextTests {
@Autowired
private MemberService memberService;
@Autowired
private UserDAO userDAO;
/**
* 可以選擇在測試開始的時候來進行mock的邏輯編寫
*/
@Before
public void mockUserDAO() {
Mockito.when(userDAO.insertMember(Mockito.any())).thenReturn(
System.currentTimeMillis());
}
@Test(expected = IllegalArgumentException.class)
public void insert_member_error() {
memberService.insertMember(null, "123");
memberService.insertMember(null, null);
}
/**
* 也可以選擇在方法中進行mock
*/
@Test(expected = IllegalArgumentException.class)
public void insert_exist_member() {
Member member = new Member();
member.setName("weipeng");
member.setPassword("123456abcd");
Mockito.when(userDAO.findMember("weipeng")).thenReturn(member);
memberService.insertMember("weipeng", "1234abc");
}
@Test(expected = IllegalArgumentException.class)
public void insert_illegal_argument() {
StringBuilder sb = new StringBuilder();
IntStream.range(0, 32).forEach(sb::append);
memberService.insertMember(sb.toString(), "abcdcsfa123");
}
@Test
public void insert_member() {
System.out.println(memberService.insertMember("windowsxp", "abc123"));
Assert.assertNotNull(memberService.insertMember("windowsxp", "abc123"));
}
}
可以看到,通過繼承AbstractJUnit4SpringContextTests
就可以完成構造applicationContext
的功能。當然通過ContextConfiguration
指明當前的配置文件所在地,就可以完成applicationContext
的初始化,同時利用Autowired
完成配置文件中的Bean的獲取。
由於在MemberService.xml
中針對UserDAO
的mock配置,對應的mock對象會被注入到MemberSpringTest
中,而後續的測試方法就可以針對它來編排mock邏輯。
我們在Before
邏輯中以及方法中均可以自由的裁剪mock邏輯,這樣JUnit
、spring-test
和Mockito
完美的統一到了一起。