一個使用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是什麼類型的。隻用在配置檔案中聲明一下就可以了。
先看一下使用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
完美的統一到了一起。