# 【第十三章】 测试 之 13.1 概述 13.2 单元测试 ——跟我学spring3
## 13.1 概述
### 13.1.1 测试
软件测试的目的首先是为了保证软件功能的正确性,其次是为了保证软件的质量,软件测试相当复杂,已经超出本书所涉及的范围,本节将只介绍软件测试流程中前两个步骤:单元测试和集成测试。
Spring提供了专门的测试模块用于简化单元测试和集成测试,单元测试和集成测试一般由程序员实现。
## 13.2 单元测试
### 13.2.1 概述
单元测试是最细粒度的测试,即具有原子性,通常测试的是某个功能(如测试类中的某个方法的功能)。
采用依赖注入后我们的代码对Spring IoC容器几乎没有任何依赖,因此在对我们代码进行测试时无需依赖Spring IoC容器,我们只需要通过简单的实例化对象、注入依赖然后测试相应方法来测试该方法是否完成我们预期的功能。
在本书中使用的传统开发流程,即先编写代码实现功能,然后再写测试来验证功能是否正确,而不是测试驱动开发,测试驱动开发是指在编写代码实现功能之前先写测试,然后再根据测试来写满足测试要求的功能代码,通过测试来驱动开发,如果对测试驱动开发感兴趣推荐阅读【测试驱动开发的艺术】。
在实际工作中,应该只对一些复杂的功能进行单元测试,对于一些简单的功能(如数据访问层的CRUD)没有必要花费时间进行单元测试。
Spring对单元测试提供如下支持:
* Mock对象:Spring通过Mock对象来简化一些场景的单元测试:
**JNDI测试支持**:在org.springframework.mock.jndi包下通过了SimpleNamingContextBuilder来来创建JNDI上下文Mock对象,从而无需依赖特定Java EE容器即可完成JNDI测试。
**web测试支持:**在org.springframework.mock.web包中提供了一组Servlet API的Mock对象,从而可以无需Web容器即可测试web层的类。
* **工具类**:通过通用的工具类来简化编写测试代码:
**反射工具类:**在org.springframework.test.util包下的ReflectionTestUtils能通过反射完成类的非public字段或setter方法的调用;
**JDBC工具类:**在org.springframework.test.util包下的SimpleJdbcTestUtils能读取一个sql脚本文件并执行来简化SQL的执行,还提供了如清空表、统计表中行数的简便方法来简化测试代码的编写。
接下来让我们学习一下开发过程中各层代码如何编写测试用例。
### 13.2.2 准备测试环境
**1、Junit安装:**将Junit 4包添加到“pointShop”项目中,具体方法请参照【2.2.3 Hello World】。
** 2、jMock安装:**到jMock官网【http://www.jmock.org/】下载最新的jMock包,在本书中使用jMock2.5.1版本,将下载的“jmock-2.5.1-jars.zip ”包中的如下jar包拷贝到项目的lib目录下并添加到类路径:
+ objenesis-1.0.jar
+ jmock-script-2.5.1.jar
+ jmock-legacy-2.5.1.jar
+ jmock-junit4-2.5.1.jar
+ jmock-junit3-2.5.1.jar
+ jmock-2.5.1.jar
+ hamcrest-library-1.1.jar
+ hamcrest-core-1.1.jar
+ bsh-core-2.0b4.jar
注:cglib包无需添加到类路径,因为我们之前已经提供。
**3、添加Spring测试支持包:**将下载的spring-framework-3.0.5.RELEASE-with-docs.zip包中的如下jar包拷贝到项目的lib目录下并添加到类路径:
dist\org.springframework.test-3.0.5.RELEASE.jar
4、在“pointShop”项目下新建test文件夹并将其添加到【Java Build Path】中,该文件夹用于存放测试代码,从而分离测试代码和开发代码。
到此测试环境搭建完毕。
### 13.2.3 数据访问层
数据访问层单元测试,目的是测试该层定义的接口实现方法的行为是否正确,其实本质是测试是否正确与数据库交互,是否发送并执行了正确的SQL,SQL执行成功后是否正确的组装了业务逻辑层需要的数据。
数据访问层单元测试通过Mock对象与数据库交互的API来完成测试。
接下来让我们学习一下如何进行数据访问层单元测试:
**1、在test文件夹下创建如下测试类:**
```
package cn.javass.point.dao.hibernate;
//省略import
public class GoodsHibernateDaoUnitTest {
//1、Mock对象上下文,用于创建Mock对象
private final Mockery context = new Mockery() {{
//1.1、表示可以支持Mock非接口类,默认只支持Mock接口
setImposteriser(ClassImposteriser.INSTANCE);
}};
//2、Mock HibernateTemplate类
private final HibernateTemplate mockHibernateTemplate = context.mock(HibernateTemplate.class);
private IGoodsDao goodsDao = null;
@Before
public void setUp() {
//3、创建IGoodsDao实现
GoodsHibernateDao goodsDaoTemp = new GoodsHibernateDao();
//4、通过ReflectionTestUtils注入需要的非public字段数据
ReflectionTestUtils.setField(goodsDaoTemp, "entityClass", GoodsModel.class);
//5、注入mockHibernateTemplate对象
goodsDaoTemp.setHibernateTemplate(mockHibernateTemplate);
//6、赋值给我们要使用的接口
goodsDao = goodsDaoTemp;
}
}
```
* Mockery:jMock核心类,用于创建Mock对象的,通过其mock方法来创建相应接口或类的Mock对象。
* goodsDaoTemp:需要测试的IGoodsDao实现,通过ReflectionTestUtils注入需要的非public字段数据。
**2、测试支持写完后,接下来测试一下IGoodsDao的get方法是否满足需求:**
```
@Test
public void testSave () {
//7、创建需要的Model数据
final GoodsModel expected = new GoodsModel();
//8、定义预期行为,并在后边来验证预期行为是否正确
context.checking(new org.jmock.Expectations() {
{
//9、表示需要调用且只调用一次mockHibernateTemplate的get方法,
//且get方法参数为(GoodsModel.class, 1),并将返回goods
one(mockHibernateTemplate).get(GoodsModel.class, 1);
will(returnValue(expected));
}
});
//10、调用goodsDao的get方法,在内部实现中将委托给
//getHibernateTemplate().get(this.entityClass, id);
//因此按照第8步定义的预期行为将返回goods
GoodsModel actual = goodsDao.get(1);
//11、来验证第8步定义的预期行为是否调用了
context.assertIsSatisfied();
//12、验证goodsDao.get(1)返回结果是否正确
Assert.assertEquals(goods, expected);
}
```
* **context.checking():**该方法中用于定义预期行为,其中第9步定义了需要调用一次且只调用一次mockHibernateTemplate的get方法,且get方法参数为(GoodsModel.class, 1),并将返回goods对象。
* **goodsDao.get(1):**调用goodsDao的get方法,在内部实现中将委托给“getHibernateTemplate().get(this.entityClass, id)”。
* **context.assertIsSatisfied():**来验证前边定义的预期行为是否执行,且是否正确。
* **Assert.assertEquals(expected, actual):**用于验证“goodsDao.get(1) ”返回的结果是否是预期结果。
以上测试方法其实是没有必要的,对于非常简单的CRUD没有必要写单元测试,只有相当复杂的方法才有必要写单元测试。
这种通过Mock对象来测试数据访问层代码其实一点意义没有,因为这里没有与数据库交互,无法验证真实环境中与数据库交互是否正确,因此这里只是告诉你如何测试数据访问层代码,在实际工作中一般通过集成测试来完成数据访问层测试。
### 13.2.4 业务逻辑层
业务逻辑单元测试,目的是测试该层的业务逻辑是否正确并通过Mock 数据访问层对象来隔离与数据库交互,从而无需连接数据库即可测试业务逻辑是否正确。
接下来让我们学习一下如何进行业务逻辑层单元测试:
**1、在test文件夹下创建如下测试类:**
```
package cn.javass.point.service.impl;
//省略import
public class GoodsCodeServiceImplUnitTest {
//1、Mock对象上下文,用于创建Mock对象
private final Mockery context = new Mockery() {{
//1.1、表示可以支持Mock非接口类,默认只支持Mock接口
setImposteriser(ClassImposteriser.INSTANCE);
}};
//2、Mock IGoodsCodeDao接口
private IGoodsCodeDao goodsCodeDao = context.mock(IGoodsCodeDao.class);;
private IGoodsCodeService goodsCodeService;
@Before
public void setUp() {
GoodsCodeServiceImpl goodsCodeServiceTemp = new GoodsCodeServiceImpl();
//3、依赖注入
goodsCodeServiceTemp.setDao(goodsCodeDao);
goodsCodeService = goodsCodeServiceTemp;
}
}
```
以上测试支持代码和数据访问层测试代码非常类似,在此不再阐述。
**2、测试支持写完后,接下来测试一下购买商品Code码是否满足需求:**
测试业务逻辑时需要分别测试多种场景,即如在某种场景下成功或失败等等,即测试应该全面,每个功能点都应该测试到。
**2.1、测试购买失败的场景:**
```
@Test(expected = NotCodeException.class)
public void testBuyFail() {
final int goodsId = 1;
//4、定义预期行为,并在后边来验证预期行为是否正确
context.checking(new org.jmock.Expectations() {
{
//5、表示需要调用goodsCodeDao对象的getOneNotExchanged一次且仅以此
//且返回值为null
one(goodsCodeDao).getOneNotExchanged(goodsId);
will(returnValue(null));
}
});
goodsCodeService.buy("test", goodsId);
context.assertIsSatisfied();
}
```
* **context.checking():**该方法中用于定义预期行为,其中第5步定义了需要调用一次且只调用一次goodsCodeDao的getOneNotExchanged方法,且getOneNotExchanged方法参数为(goodsId),并将返回null。
* **goodsCodeService.buy("test", goodsId):**调用goodsCodeService的buy方法,由于调用goodsCodeDao的getOneNotExchanged方法将返回null,因此buy方法将抛出“NotCodeException”异常,从而表示没有Code码。
* **context.assertIsSatisfied():**来验证前边定义的预期行为是否执行,且是否正确。
* 由于我们在预期行为中调用getOneNotExchanged将返回null,因此测试将失败且抛出NotCodeException异常。
**2.2、测试购买成功的场景:**
```
@Test()
public void testBuySuccess () {
final int goodsId = 1;
final GoodsCodeModel goodsCode = new GoodsCodeModel();
//6、定义预期行为,并在后边来验证预期行为是否正确
context.checking(new org.jmock.Expectations() {
{
//7、表示需要调用goodsCodeDao对象的getOneNotExchanged一次且仅以此
//且返回值为null
one(goodsCodeDao).getOneNotExchanged(goodsId);
will(returnValue(goodsCode));
//8、表示需要调用goodsCodeDao对象的save方法一次且仅一次
//且参数为goodsCode
one(goodsCodeDao).save(goodsCode);
}
});
goodsCodeService.buy("test", goodsId);
context.assertIsSatisfied();
Assert.assertEquals(goodsCode.isExchanged(), true);
}
```
* **context.checking():**该方法中用于定义预期行为,其中第7步定义了需要调用一次且只调用一次goodsCodeDao的getOneNotExchanged方法,且getOneNotExchanged方法参数为(goodsId),并将返回goodsCode对象;第8步定义了需要调用goodsCodeDao对象的save一次且仅一次。
* **goodsCodeService.buy("test", goodsId):**调用goodsCodeService的buy方法,由于调用goodsCodeDao的getOneNotExchanged方法将返回goodsCode,因此buy方法将成功执行。
* **context.assertIsSatisfied():**来验证前边定义的预期行为是否执行,且是否正确。
* **Assert.assertEquals(goodsCode.isExchanged(), true):**表示goodsCode已经被购买过了。
* 由于我们在预期行为中调用getOneNotExchanged将返回一个goodsCode对象,因此测试将成功,如果失败说明业务逻辑写错了。
到此业务逻辑层测试完毕,在进行业务逻辑层测试时我们只关心业务逻辑是否正确,而不关心底层数据访问层如何实现,因此测试业务逻辑层时只需Mock 数据访问层对象,然后定义一些预期行为来满足业务逻辑测试需求即可。
### 13.2.5 表现层
表现层测试包括如Struts2的Action单元测试、拦截器单元测试、JSP单元测试等等,在此我们只学习Struts2的Action单元测试。
Struts2的Action测试相对业务逻辑层测试相对复杂一些,因为牵扯到使用如Servlet API、ActionContext等等,这里需要通过stub(桩)实现或mock对象来模拟如HttpServletRequest等对象。
一、首先学习一些最简单的Action测试:
**1、在test文件夹下创建如下测试类:**
```
package cn.javass.point.web.front;
import cn.javass.point.service.IGoodsCodeService;
import cn.javass.point.web.front.action.GoodsAction;
//省略部分import
public class GoodsActionUnitTest {
//1、Mock对象上下文,用于创建Mock对象
private final Mockery context = new Mockery() {{
//1.1、表示可以支持Mock非接口类,默认只支持Mock接口
setImposteriser(ClassImposteriser.INSTANCE);
}};
//2、Mock IGoodsCodeService接口
private IGoodsCodeService goodsCodeService = context.mock(IGoodsCodeService.class);
private GoodsAction goodsAction;
@Before
public void setUp() {
goodsAction = new GoodsAction();
//3、依赖注入
goodsAction.setGoodsCodeService(goodsCodeService);
}
}
```
以上测试支持代码和业务逻辑层测试代码非常类似,在此不再阐述。
**2、测试支持写完后,接下来测试一下前台购买商品Code码是否满足需求:**
类似于测试业务逻辑时需要分别测试多种场景,测试Action同样需要分别测试多种场景。
**2.1、测试购买失败的场景:**
```
@Test
public void testBuyFail() {
final int goodsId = 1;
//4、定义预期行为,并在后边来验证预期行为是否正确
context.checking(new org.jmock.Expectations() {
{
//5、表示需要调用goodsCodeService对象的buy方法一次且仅一次
//且抛出NotCodeException异常
one(goodsCodeService).buy("test", goodsId);
will(throwException(new NotCodeException()));
}
});
//6、模拟Struts注入请求参数
goodsAction.setGoodsId(goodsId);
String actualResultCode = goodsAction.buy();
context.assertIsSatisfied();
Assert.assertEquals(ReflectionTestUtils.getField(goodsAction, "BUY_RESULT"), actualResultCode);
Assert.assertTrue(goodsAction.getActionErrors().size() > 0);
}
```
* **context.checking():**该方法中用于定义预期行为,其中第5步定义了需要调用goodsCodeService对象的buy方法一次且仅一次且将抛出NotCodeException异常。
* **goodsAction.setGoodsId(goodsId)**:用于模拟Struts注入请求参数,即完成数据绑定。
* **goodsAction.buy():**调用goodsAction的buy方法,该方法将委托给IGoodsCodeService实现完成,返回值用于定位视图。
* **context.assertIsSatisfied():**来验证前边定义的预期行为是否执行,且是否正确。
* **Assert.assertEquals(ReflectionTestUtils.getField(goodsAction, "BUY_RESULT"), actualResultCode)**:验证返回的Result是否是我们指定的。
* **Assert.assertTrue(goodsAction.getActionErrors().size() > 0)**:表示执行Action时有错误,即Action动作错误。如果条件不成立,说明我们Action功能是错误的,需要修改。
**2.2、测试购买成功的场景:**
```
@Test
public void testBuySuccess() {
final int goodsId = 1;
final GoodsCodeModel goodsCode = new GoodsCodeModel();
//7、定义预期行为,并在后边来验证预期行为是否正确
context.checking(new org.jmock.Expectations() {
{
//8、表示需要调用goodsCodeService对象的buy方法一次且仅一次
//且返回goodsCode对象
one(goodsCodeService).buy("test", goodsId);
will(returnValue(goodsCode));
}
});
//9、模拟Struts注入请求参数
goodsAction.setGoodsId(goodsId);
String actualResultCode = goodsAction.buy();
context.assertIsSatisfied();
Assert.assertEquals(ReflectionTestUtils.getField(goodsAction, "BUY_RESULT"), actualResultCode);
Assert.assertTrue(goodsAction.getActionErrors().size() == 0);
}
```
* **context.checking():**该方法中用于定义预期行为,其中第5步定义了需要调用goodsCodeService对象的buy方法一次且仅一次且将返回goodsCode对象。
* **goodsAction.setGoodsId(goodsId)**:用于模拟Struts注入请求参数,即完成数据绑定。
* **goodsAction.buy():**调用goodsAction的buy方法,该方法将委托给IGoodsCodeService实现完成,返回值用于定位视图。
* **context.assertIsSatisfied():**来验证前边定义的预期行为是否执行,且是否正确。
* **Assert.assertEquals(ReflectionTestUtils.getField(goodsAction, "BUY_RESULT"), actualResultCode)**:验证返回的Result是否是我们指定的。
* **Assert.assertTrue(goodsAction.getActionErrors().size() == 0)**:表示执行Action时没有错误,即Action动作正确。如果条件不成立,说明我们Action功能是错误的,需要修改。
通过模拟ActionContext对象内容从而可以非常容易的测试Action中各种与http请求相关情况,无需依赖web服务器即可完成测试。但对于如果我们使用htpp请求相关对象的该如何测试?如果我们需要使用ActionContext获取值栈数据应该怎么办?这就需要Struts提供的junit插件支持了。我们会在集成测试中介绍。
对于表现层其他功能的单元测试本书不再介绍,如JSP单元测试、拦截器单元测试等等。
原创内容,转载请注明私塾在线【[http://sishuok.com/forum/blogPost/list/0/2555.html](http://sishuok.com/forum/blogPost/list/0/2555.html#7330)】
- 跟我学 Spring3
- 【第二章】 IoC 之 2.1 IoC基础 ——跟我学Spring3
- 【第二章】 IoC 之 2.2 IoC 容器基本原理 ——跟我学Spring3
- 【第二章】 IoC 之 2.3 IoC的配置使用——跟我学Spring3
- 【第三章】 DI 之 3.1 DI的配置使用 ——跟我学spring3
- 【第三章】 DI 之 3.2 循环依赖 ——跟我学spring3
- 【第三章】 DI 之 3.3 更多DI的知识 ——跟我学spring3
- 【第三章】 DI 之 3.4 Bean的作用域 ——跟我学spring3
- 【第四章】 资源 之 4.1 基础知识 ——跟我学spring3
- 【第四章】 资源 之 4.2 内置Resource实现 ——跟我学spring3
- 【第四章】 资源 之 4.3 访问Resource ——跟我学spring3
- 【第四章】 资源 之 4.4 Resource通配符路径 ——跟我学spring3
- 【第五章】Spring表达式语言 之 5.1 概述 5.2 SpEL基础 ——跟我学spring3
- 【第五章】Spring表达式语言 之 5.3 SpEL语法 ——跟我学spring3
- 【第五章】Spring表达式语言 之 5.4在Bean定义中使用EL—跟我学spring3
- 【第六章】 AOP 之 6.1 AOP基础 ——跟我学spring3
- 【第六章】 AOP 之 6.2 AOP的HelloWorld ——跟我学spring3
- 【第六章】 AOP 之 6.3 基于Schema的AOP ——跟我学spring3
- 【第六章】 AOP 之 6.4 基于@AspectJ的AOP ——跟我学spring3
- 【第六章】 AOP 之 6.5 AspectJ切入点语法详解 ——跟我学spring3
- 【第六章】 AOP 之 6.6 通知参数 ——跟我学spring3
- 【第六章】 AOP 之 6.7 通知顺序 ——跟我学spring3
- 【第六章】 AOP 之 6.8 切面实例化模型 ——跟我学spring3
- 【第六章】 AOP 之 6.9 代理机制 ——跟我学spring3
- 【第七章】 对JDBC的支持 之 7.1 概述 ——跟我学spring3
- 【第七章】 对JDBC的支持 之 7.2 JDBC模板类 ——跟我学spring3
- 【第七章】 对JDBC的支持 之 7.3 关系数据库操作对象化 ——跟我学spring3
- 【第七章】 对JDBC的支持 之 7.4 Spring提供的其它帮助 ——跟我学spring3【私塾在线原创】
- 【第七章】 对JDBC的支持 之 7.5 集成Spring JDBC及最佳实践 ——跟我学spring3
- 【第八章】 对ORM的支持 之 8.1 概述 ——跟我学spring3
- 【第八章】 对ORM的支持 之 8.2 集成Hibernate3 ——跟我学spring3
- 【第八章】 对ORM的支持 之 8.3 集成iBATIS ——跟我学spring3
- 【第八章】 对ORM的支持 之 8.4 集成JPA ——跟我学spring3
- 【第九章】 Spring的事务 之 9.1 数据库事务概述 ——跟我学spring3
- 【第九章】 Spring的事务 之 9.2 事务管理器 ——跟我学spring3
- 【第九章】 Spring的事务 之 9.3 编程式事务 ——跟我学spring3
- 【第九章】 Spring的事务 之 9.4 声明式事务 ——跟我学spring3
- 【第十章】集成其它Web框架 之 10.1 概述 ——跟我学spring3
- 【第十章】集成其它Web框架 之 10.2 集成Struts1.x ——跟我学spring3
- 【第十章】集成其它Web框架 之 10.3 集成Struts2.x ——跟我学spring3
- 【第十章】集成其它Web框架 之 10.4 集成JSF ——跟我学spring3
- 【第十一章】 SSH集成开发积分商城 之 11.1 概述 ——跟我学spring3
- 【第十一章】 SSH集成开发积分商城 之 11.2 实现通用层 ——跟我学spring3
- 【第十一章】 SSH集成开发积分商城 之 11.3 实现积分商城层 ——跟我学spring3
- 【第十二章】零配置 之 12.1 概述 ——跟我学spring3
- 【第十二章】零配置 之 12.2 注解实现Bean依赖注入 ——跟我学spring3
- 【第十二章】零配置 之 12.3 注解实现Bean定义 ——跟我学spring3
- 【第十二章】零配置 之 12.4 基于Java类定义Bean配置元数据 ——跟我学spring3
- 【第十二章】零配置 之 12.5 综合示例-积分商城 ——跟我学spring3
- 【第十三章】 测试 之 13.1 概述 13.2 单元测试 ——跟我学spring3
- 【第十三章】 测试 之 13.3 集成测试 ——跟我学spring3
- 跟我学 Spring MVC
- SpringMVC + spring3.1.1 + hibernate4.1.0 集成及常见问题总结
- Spring Web MVC中的页面缓存支持 ——跟我学SpringMVC系列
- Spring3 Web MVC下的数据类型转换(第一篇)——《跟我学Spring3 Web MVC》抢先看
- Spring3 Web MVC下的数据格式化(第二篇)——《跟我学Spring3 Web MVC》抢先看
- 第一章 Web MVC简介 —— 跟开涛学SpringMVC
- 第二章 Spring MVC入门 —— 跟开涛学SpringMVC
- 第三章 DispatcherServlet详解 ——跟开涛学SpringMVC
- 第四章 Controller接口控制器详解(1)——跟着开涛学SpringMVC
- 第四章 Controller接口控制器详解(2)——跟着开涛学SpringMVC
- 第四章 Controller接口控制器详解(3)——跟着开涛学SpringMVC
- 第四章 Controller接口控制器详解 (4)——跟着开涛学SpringMVC
- 第四章 Controller接口控制器详解(5)——跟着开涛学SpringMVC
- 跟着开涛学SpringMVC 第一章源代码下载
- 第二章 Spring MVC入门 源代码下载
- 第四章 Controller接口控制器详解 源代码下载
- 第四章 Controller接口控制器详解(6)——跟着开涛学SpringMVC
- 第四章 Controller接口控制器详解(7 完)——跟着开涛学SpringMVC
- 第五章 处理器拦截器详解——跟着开涛学SpringMVC
- 源代码下载 第五章 处理器拦截器详解——跟着开涛学SpringMVC
- 注解式控制器运行流程及处理器定义 第六章 注解式控制器详解——跟着开涛学SpringMVC
- 源代码下载 第六章 注解式控制器详解
- SpringMVC3强大的请求映射规则详解 第六章 注解式控制器详解——跟着开涛学SpringMVC
- Spring MVC 3.1新特性 生产者、消费者请求限定 —— 第六章 注解式控制器详解——跟着开涛学SpringMVC
- SpringMVC强大的数据绑定(1)——第六章 注解式控制器详解——跟着开涛学SpringMVC
- SpringMVC强大的数据绑定(2)——第六章 注解式控制器详解——跟着开涛学SpringMVC
- SpringMVC数据类型转换——第七章 注解式控制器的数据验证、类型转换及格式化——跟着开涛学SpringMVC
- SpringMVC数据格式化——第七章 注解式控制器的数据验证、类型转换及格式化——跟着开涛学SpringMVC
- SpringMVC数据验证——第七章 注解式控制器的数据验证、类型转换及格式化——跟着开涛学SpringMVC