前言
引言
"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,首先添加依赖:
org.mockito
mockito-all
接下来尝试对java.util.List
进行Mock,Mock对于List操作的内容进行构造。
构造Mock
先看一下最简的使用方式。
public void mock_one() {
List 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 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 {
/**
*
* 插入一个会员,返回会员的主键
* 如果有重复,则会抛出异常
*
*
* @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版本一致
org.springframework
spring-test
test
配置
由于Mockito
支持mock
方法构造,所以我们可以将它通过spring factory bean的形式融入到 spring 的体系中。我们针对MemberService
进行测试,需要对UserDAO
进行Mock,我们只需要在配置中配置即可。
配置在MemberService.xml中,这里需要说明一下 没有使用共用的配置文件, 目的就是让大家在测试的时候能够相互独立,而且在一个配置文件中配置的Bean越多,就证明你要测试的类依赖越复杂,也就是越不合理,逼迫自己做重构。
com.murdock.tools.mockito.dao.UserDAO
在进行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
完美的统一到了一起。
现代化的spring-test使用方式
以下例子可以在
javaconfig-spring-test
中找到。
在classic-spring-test
中演示的单元测试,还是用配置文件的方式,但是从Spring4之后,官方就鼓励使用Java的方式对spring进行配置,而不是用以前那样的xml配置形式了,因此我们基于注解可以来简化单元测试的编写,我们称之为现代化的spring-test
方式。
修改单元测试
测试不用继承AbstractJUnit4SpringContextTests
,通过注解即可,然后对于bean的配置,可以通过Java配置风格完成
注解
使用RunWith
和ContextConfiguration
配置即可将一个类声明为支持Spring容器的测试用例。
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = MemberJavaConfigTest.MemberServiceConfig.class)
public class MemberJavaConfigTest {
}
注解 | 说明 |
---|---|
RunWith |
该注解是junit 提供的,表示用那种方式来执行这个测试,这里是SpringRunner ,由spring-test 提供 |
ContextConfiguration |
对测试的Spring容器的配置,比如:配置的位置等 |
配置与示例
通过注解可以声明按照何种方式去执行测试,以及测试的Spring容器如何组装,但是还或缺在Spring容器中如何配置Bean,以前这是通过xml来进行配置的。
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = MemberJavaConfigTest.MemberServiceConfig.class)
public class MemberJavaConfigTest {
@Autowired
private MemberService memberService;
@Test
public void insert_member() {
System.out.println(memberService.insertMember("windowsxp", "abc123"));
Assert.assertNotNull(memberService.insertMember("windowsxp", "abc123"));
}
@Configuration
static class MemberServiceConfig {
@Bean
public MemberService memberService(UserDAO userDAO) {
MemberServiceImpl memberService = new MemberServiceImpl();
memberService.setUserDAO(userDAO);
return memberService;
}
@Bean
public UserDAO userDAO() {
UserDAO mock = Mockito.mock(UserDAO.class);
Mockito.when(mock.insertMember(Mockito.any())).thenReturn(System.currentTimeMillis());
return mock;
}
}
}
可以看到只需要有一个类,被注解了Configuration
,该类就是一个配置类型,而这种Java Config Style已经是Spring官方推荐的方式了。
Bean
注解类似xml中的bean
标签,这里配置了两个Bean一个MemberService
的实现,另外一个是mock的UserDAO
。其中对MemberService
的配置需要依赖UserDAO
。
剩下的测试过程就和之前classic-spring-test
完全一致了,可以看到新的方式没有了恼人的xml配置,变得更加直接和高效。