在过去的职业生涯里,我经常发现有些人不写测试代码,而他们声称不写的理由是无法轻易地写出覆盖多个不同模块的测试用例。好吧,我相信他们中的大部分要么是缺乏一些比较易掌握的技术手段,要么就是没时间来把它搞清楚,毕竟工作中总会有进度之类的各种压力。因为不知道该如何测试,所以就经常忽略集成测试,由此带来的问题就是越来越糟糕的软件、越来越多的BUG和更加失望的客户。所以我想分享一些个人的经验,揭开集成测试神秘的面纱。
如何对基于Spring的工程更好地进行集成测试
使用工具: Spring, JUnit, Mockito
想象有这样一个Spring工程,它集成了一些外部服务,例如,一些银行的web服务。那么,为这个工程写测试用例以及在持续集成系统中完成这些测试时所遇到的问题基本都差不多:
1.每次测试都会有交易进行,每次交易都需要付出金钱成本,这些成本最终由客户承担;
2.测试时发出的过多的请求有可能被认为是恶意请求,可能造成在银行的账户被封,后果是测试失败;
3.当使用非生产环境进行测试时,测试结果并不十分可靠,同样,后果是测试失败。
通常情况下,你对单个类进行测试的时候,问题很容易解决,因为你可以虚拟一些外部服务来供调用。但是当对整个巨大的业务流程进行测试的时候,意味你需要对多个部件进行测试,这时,需要你将这些部件都纳入到Spring容器中进行管理。所幸,Spring包含了非常优秀的测试框架,允许你将来自生产环境配置文件中的bean注入到测试环境中,但是对那些被调用的外部服务,需要我们自己去写模拟实现。一般人第一反应可能是在测试的setUp阶段对由Spring注入的bean进行重新注入(修改),但是这种方法需要再仔细考虑一下。
警告:通过这种方式,你的测试代码打破了容器自身的行为,所以没法保证在真实的环境中也如你测试的结果一样。
事实上,我们无需先实现模拟类然后再把它重新注入到所需的bean中,我们可以让Spring帮助我们一开始就注入模拟类。让我们用代码演示一下。
示例工程包含一个名为BankService的类,代表调用的外部服务,一个名为UserBalanceService的类,它会调用BankService。UserBalanceService实现的非常简单,仅仅完成将余额从String向Double类型的转换。
BankService.java的源码:
1
2
3
|
public interface BankService { String getBalanceByEmail(String email); } |
BankServiceImpl.java的源码:
1
2
3
4
5
6
|
public class BankServiceImpl implements BankService { @Override public String getBalanceByEmail(String email) { throw new UnsupportedOperationException( "Operation failed due to external exception" ); } } |
UserBalanceService.java的源码:
1
2
3
|
interface UserBalanceService { Double getAccountBalance(String email); } |
UserBalanceServiceImpl.java的源码:
1
2
3
4
5
6
7
8
|
public class UserBalanceServiceImpl implements UserBalanceService { @Autowired private BankService bankService; @Override public Double getAccountBalance(String email) { return Double.valueOf(bankService.getBalanceByEmail(email)); } } |
然后是Spring的XML配置文件,添加所需要的bean声明。
applicationContext.xml的源代码:
1
2
3
4
5
6
7
8
|
<? xml version = "1.0" encoding = "UTF-8" ?> xsi:schemaLocation="http://www.springframework.org/schema/beans < bean id = "bankService" class = "ua.eshepelyuk.blog.springtest.springockito.BankServiceImpl" /> < bean id = "userBalanceService" class = "ua.eshepelyuk.blog.springtest.springockito.UserBalanceServiceImpl" /> </ beans > |
下面是测试类UserBalanceServiceImplTest.java的源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@RunWith (SpringJUnit4ClassRunner. class ) @ContextConfiguration (locations = "classpath:/springtest/springockito/applicationContext.xml" ) public class UserBalanceServiceImplProfileTest { @Autowired private UserBalanceService userBalanceService; @Autowired private BankService bankService; @Test public void shouldReturnMockedBalance() { Double balance = userBalanceService.getAccountBalance( "user@bank.com" ); assertEquals(balance, Double.valueOf( 123 .45D)); } } |
如我们预料的一样,测试方法报UnsupportedOperationException异常。我们现在的目的是把BankService换成我们的模拟实现。直接使用Mockito来生成factory bean的方法是没问题的,但是有更好的选择,使用Springockito框架。继续之前可以先大概了解一下。
剩下的问题就简单了:如何让Spring注入模拟的bean而不是真实的bean,在Spring 3.1版之前除了新建一个XML配置文件之外没有其他的方法。但是自从Spring引入了bean的profile定义之后,我们有了更加优雅的解决方式,虽然这种方式也需要一个额外的专门用作测试的XML配置文件。下面是这个用来测试的配置文件testApplicationContext.xml的代码:
1
2
3
4
5
6
7
8
9
10
11
|
<? xml version = "1.0" encoding = "UTF-8" ?> xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.mockito.org/spring/mockito https://bitbucket.org/kubek2k/springockito/raw/tip/springockito/src/main/resources/spring/mockito.xsd"> < import resource = "classpath:/springtest/springockito/applicationContext.xml" /> < beans profile = "springTest" > < mockito:mock id = "bankService" class = "ua.eshepelyuk.blog.springtest.springockito.BankService" /> </ beans > </ beans > |
做相应修改过之后的测试类UserBalanceServiceImplProfileTest.java的源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@RunWith (SpringJUnit4ClassRunner. class ) @ContextConfiguration (locations = "classpath:/springtest/springockito/testApplicationContext.xml" ) @ActiveProfiles (profiles = { "springTest" }) public class UserBalanceServiceImplProfileTest { @Autowired private UserBalanceService userBalanceService; @Autowired private BankService bankService; @Before public void setUp() throws Exception { Mockito.when(bankService.getBalanceByEmail( "user@bank.com" )).thenReturn(String.valueOf( 123 .45D)); } @Test public void shouldReturnMockedBalance() { Double balance = userBalanceService.getAccountBalance( "user@bank.com" ); assertEquals(balance, Double.valueOf( 123 .45D)); } } |
你可能注意到了,在setUp方法里,我们定义了模拟的行为,并且在类上面加了@Profile的注解。这个注解激活了名为springTest的profile,因此使用Springockito模拟的bean就可以自动注入到任何它所需要的地方了。这个测试的运行结果会成功,因为Spring注入了Springockito 所模拟的版本,而不是applicationContext.xml里所声明的版本。
继续优化我们的测试
如果我们能将解决这个问题的方法更加推进一步的话,这篇文章看起来才没有缺憾。Springockito提供了另外一个名字叫作
Springockito Annotation的框架,它允许我们在测试类中使用注解来注入模拟类。继续看下去之前,您最好先去网站上大概瞧瞧。好了,下面是经过修改后的测试代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
UserBalanceServiceImplAnnotationTest.java的源代码: @RunWith (SpringJUnit4ClassRunner. class ) @ContextConfiguration (loader = SpringockitoContextLoader. class , locations = "classpath:/springtest/springockito/applicationContext.xml" ) public class UserBalanceServiceImplAnnotationTest { @Autowired private UserBalanceService userBalanceService; @Autowired @ReplaceWithMock private BankService bankService; @Before public void setUp() throws Exception { Mockito.when(bankService.getBalanceByEmail( "user@bank.com" )).thenReturn(String.valueOf(valueOf( 123 .45D))); } @Test public void shouldReturnMockedBalance() { Double balance = userBalanceService.getAccountBalance( "user@bank.com" ); assertEquals(balance, valueOf( 123 .45D)); } } |
请注意,这里并没有新引入的XML配置文件,而是直接使用了正式环境的applicationContext.xml。我们使用@ReplaceWithMock这个注解标记了类型为BankService的bean,而后在setUp方法中对模拟类的行为进行了定义。
后记
Springockito-annotations项目有个巨大的优点,那就是,它使我们的测试代码建立在依赖覆盖的基础之上,通过这样,我们既不需要定义额外的XML配置文件,也不需要为了测试而去改动生产环境的配置文件。如果不使用Springockito-annotations的话,我们除了定义额外的XML配置文件别无他选了。因此,我强烈建议您在集成测试中使用Springockito-annotations,这样你可以最大限度减少测试用例对生产代码的影响,也能消除维护额外XML配置文件的负担。
附言
为Spring工程写集成测试真是简单多了吧,文章中的代码参考自我的GitHub。
译文链接:http://www.codeceo.com/article/spring-test-is-easy.html
英文原文:Test Me If You Can #1 (Spring Framework)
翻译作者:码农网 – Sandbox Wang
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。