知识点

相关文章

更多

最近更新

更多

【第十三章】 测试 之 13.1 概述 13.2 单元测试 ——跟我学spring3

2019-03-01 17:22|来源: 开涛

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文件夹下创建如下测试类:

  1. package cn.javass.point.dao.hibernate;  

  2. //省略import  

  3. public class GoodsHibernateDaoUnitTest {  

  4.    //1、Mock对象上下文,用于创建Mock对象  

  5.    private final Mockery context = new Mockery() {{  

  6.        //1.1、表示可以支持Mock非接口类,默认只支持Mock接口  

  7.        setImposteriser(ClassImposteriser.INSTANCE);  

  8.    }};  

  9.    //2、Mock HibernateTemplate类  

  10.    private final HibernateTemplate mockHibernateTemplate = context.mock(HibernateTemplate.class);  

  11.    private IGoodsDao goodsDao = null;  

  12.  

  13.    @Before  

  14.    public void setUp() {  

  15.        //3、创建IGoodsDao实现  

  16.        GoodsHibernateDao goodsDaoTemp = new GoodsHibernateDao();  

  17.        //4、通过ReflectionTestUtils注入需要的非public字段数据  

  18.        ReflectionTestUtils.setField(goodsDaoTemp, "entityClass", GoodsModel.class);  

  19.        //5、注入mockHibernateTemplate对象  

  20.        goodsDaoTemp.setHibernateTemplate(mockHibernateTemplate);  

  21.        //6、赋值给我们要使用的接口  

  22.        goodsDao = goodsDaoTemp;  

  23. }  

  24. }  

  • Mockery:jMock核心类,用于创建Mock对象的,通过其mock方法来创建相应接口或类的Mock对象。

  • goodsDaoTemp:需要测试的IGoodsDao实现,通过ReflectionTestUtils注入需要的非public字段数据。

2、测试支持写完后,接下来测试一下IGoodsDao的get方法是否满足需求:

  1. @Test  

  2. public void testSave () {  

  3.    //7、创建需要的Model数据  

  4.    final GoodsModel expected = new GoodsModel();  

  5.    //8、定义预期行为,并在后边来验证预期行为是否正确  

  6.    context.checking(new org.jmock.Expectations() {  

  7.        {  

  8.            //9、表示需要调用且只调用一次mockHibernateTemplate的get方法,  

  9.            //且get方法参数为(GoodsModel.class, 1),并将返回goods  

  10.            one(mockHibernateTemplate).get(GoodsModel.class, 1);  

  11.            will(returnValue(expected));  

  12.         }  

  13.    });  

  14.    //10、调用goodsDao的get方法,在内部实现中将委托给  

  15.    //getHibernateTemplate().get(this.entityClass, id);  

  16.    //因此按照第8步定义的预期行为将返回goods  

  17.    GoodsModel actual = goodsDao.get(1);  

  18.    //11、来验证第8步定义的预期行为是否调用了  

  19.   context.assertIsSatisfied();  

  20.    //12、验证goodsDao.get(1)返回结果是否正确  

  21.    Assert.assertEquals(goods, expected);  

  22. }  

  23.  

  24.  

  • 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文件夹下创建如下测试类:

  1. package cn.javass.point.service.impl;  

  2. //省略import  

  3. public class GoodsCodeServiceImplUnitTest {  

  4.    //1、Mock对象上下文,用于创建Mock对象  

  5.    private final Mockery context = new Mockery() {{  

  6.        //1.1、表示可以支持Mock非接口类,默认只支持Mock接口  

  7.        setImposteriser(ClassImposteriser.INSTANCE);  

  8.    }};  

  9.    

  10.    //2、Mock IGoodsCodeDao接口  

  11.    private IGoodsCodeDao goodsCodeDao = context.mock(IGoodsCodeDao.class);;  

  12.    

  13.    private IGoodsCodeService goodsCodeService;  

  14.  

  15.    @Before  

  16.    public void setUp() {  

  17.        GoodsCodeServiceImpl goodsCodeServiceTemp = new GoodsCodeServiceImpl();  

  18.        //3、依赖注入  

  19.        goodsCodeServiceTemp.setDao(goodsCodeDao);  

  20.        goodsCodeService = goodsCodeServiceTemp;  

  21. }  

  22. }  

   以上测试支持代码和数据访问层测试代码非常类似,在此不再阐述。

2、测试支持写完后,接下来测试一下购买商品Code码是否满足需求:

测试业务逻辑时需要分别测试多种场景,即如在某种场景下成功或失败等等,即测试应该全面,每个功能点都应该测试到。

2.1、测试购买失败的场景:

  1. @Test(expected = NotCodeException.class)  

  2. public void testBuyFail() {  

  3.    final int goodsId = 1;  

  4.    //4、定义预期行为,并在后边来验证预期行为是否正确  

  5.    context.checking(new org.jmock.Expectations() {  

  6.      {  

  7.          //5、表示需要调用goodsCodeDao对象的getOneNotExchanged一次且仅以此  

  8.          //且返回值为null  

  9.          one(goodsCodeDao).getOneNotExchanged(goodsId);  

  10.          will(returnValue(null));  

  11.      }  

  12.    });  

  13.    goodsCodeService.buy("test", goodsId);  

  14.    context.assertIsSatisfied();  

  15. }  

  • 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、测试购买成功的场景:

  1. @Test()  

  2. public void testBuySuccess () {  

  3.    final int goodsId = 1;  

  4.    final GoodsCodeModel goodsCode = new GoodsCodeModel();  

  5.    //6、定义预期行为,并在后边来验证预期行为是否正确  

  6.    context.checking(new org.jmock.Expectations() {  

  7.      {  

  8.          //7、表示需要调用goodsCodeDao对象的getOneNotExchanged一次且仅以此  

  9.          //且返回值为null  

  10.          one(goodsCodeDao).getOneNotExchanged(goodsId);  

  11.          will(returnValue(goodsCode));  

  12.          //8、表示需要调用goodsCodeDao对象的save方法一次且仅一次  

  13.          //且参数为goodsCode  

  14.          one(goodsCodeDao).save(goodsCode);  

  15.        }  

  16.      });  

  17.    goodsCodeService.buy("test", goodsId);  

  18.    context.assertIsSatisfied();  

  19.    Assert.assertEquals(goodsCode.isExchanged(), true);  

  20. }  

  • 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文件夹下创建如下测试类:

  1. package cn.javass.point.web.front;  

  2. import cn.javass.point.service.IGoodsCodeService;  

  3. import cn.javass.point.web.front.action.GoodsAction;  

  4. //省略部分import  

  5. public class GoodsActionUnitTest {  

  6.    //1、Mock对象上下文,用于创建Mock对象  

  7.    private final Mockery context = new Mockery() {{  

  8.        //1.1、表示可以支持Mock非接口类,默认只支持Mock接口  

  9.        setImposteriser(ClassImposteriser.INSTANCE);  

  10.    }};  

  11.    

  12.    //2、Mock IGoodsCodeService接口  

  13.    private IGoodsCodeService goodsCodeService = context.mock(IGoodsCodeService.class);  

  14.  

  15.    private GoodsAction goodsAction;  

  16.  

  17.    @Before  

  18.    public void setUp() {  

  19.        goodsAction = new GoodsAction();  

  20.        //3、依赖注入  

  21.        goodsAction.setGoodsCodeService(goodsCodeService);  

  22. }  

  23. }  

   以上测试支持代码和业务逻辑层测试代码非常类似,在此不再阐述。

2、测试支持写完后,接下来测试一下前台购买商品Code码是否满足需求:

类似于测试业务逻辑时需要分别测试多种场景,测试Action同样需要分别测试多种场景。

2.1、测试购买失败的场景:

  1. @Test  

  2. public void testBuyFail() {  

  3.    final int goodsId = 1;  

  4.    //4、定义预期行为,并在后边来验证预期行为是否正确  

  5.    context.checking(new org.jmock.Expectations() {  

  6.      {  

  7.          //5、表示需要调用goodsCodeService对象的buy方法一次且仅一次  

  8.          //且抛出NotCodeException异常  

  9.          one(goodsCodeService).buy("test", goodsId);  

  10.      will(throwException(new NotCodeException()));  

  11.      }  

  12.    });  

  13.    //6、模拟Struts注入请求参数  

  14.    goodsAction.setGoodsId(goodsId);  

  15.    String actualResultCode = goodsAction.buy();  

  16.    context.assertIsSatisfied();  

  17.    Assert.assertEquals(ReflectionTestUtils.getField(goodsAction, "BUY_RESULT"), actualResultCode);  

  18.    Assert.assertTrue(goodsAction.getActionErrors().size() > 0);  

  19. }  

  • 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、测试购买成功的场景:

  1. @Test  

  2. public void testBuySuccess() {  

  3.    final int goodsId = 1;  

  4.    final GoodsCodeModel goodsCode = new GoodsCodeModel();  

  5.    //7、定义预期行为,并在后边来验证预期行为是否正确  

  6.    context.checking(new org.jmock.Expectations() {  

  7.      {  

  8.          //8、表示需要调用goodsCodeService对象的buy方法一次且仅一次  

  9.          //且返回goodsCode对象  

  10.          one(goodsCodeService).buy("test", goodsId);  

  11.          will(returnValue(goodsCode));  

  12.      }  

  13.    });  

  14.    //9、模拟Struts注入请求参数  

  15.    goodsAction.setGoodsId(goodsId);  

  16.    String actualResultCode = goodsAction.buy();  

  17.    context.assertIsSatisfied();  

  18.    Assert.assertEquals(ReflectionTestUtils.getField(goodsAction, "BUY_RESULT"), actualResultCode);  

  19.    Assert.assertTrue(goodsAction.getActionErrors().size() == 0);  

  20. }  

  • 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

相关问答

更多
  • 用System.out.println()输出一个对象的时候,java默认调用对象的toString()方法 一般你要覆盖这个方法,这样根据覆盖逻辑你就可以输出自己的对象 比如你定义一个类User,有id,name属性,你直接输出一个user对象的话 System.out.println(user),得到的只是 全限定名@地址首地址 如果你在User类里面覆盖这个toString方法的话就能输出你要的 比如 public String toString(){ return "user name is:"+t ...
  • 1. 测试一般的类 写一个抽象类,所有的测试类都继承它[java] @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "classpath*:application-context-test.xml" }) public abstract class AbstractJUnit { } [java] public class OriginAndDestinationServiceTestCase exten ...
  • 如何给spring3 MVC中的Action做JUnit单元测试   输出的效果如下:   信息前面会出现一个带叉的红色圆形图标。   这个效果要比警告信息更友好了,字体颜色成红色了。   要更牛叉莫过于对文字应用样式。而现在这一特性已经在谷歌浏览器里实现了。   在Chrome的开发者工具里,console 可以加样式,可以显示缤纷的颜色,甚至图片。简直爽翻了。   具体来说,是可以对输出到console控制台的文字进行CSS控制。   格式如下:   console.log("%c需要输出的信息 ", ...
  • 由于当前最想版本的Spring(Test) 3.0.5还不支持@ContextConfiguration的注解式context file注入,所以还需要写个setUp处理下,否则类似于Tiles的加载过程会有错误,因为没有ServletContext。3.1的版本应该有更好的解决方案。
  • 需要你初始化连接池再做单元测试。 一般的方法是写一些专为测试用的连接池。
  • 单元测试是在软件开发过程中要进行的最低级别的测试活动,在单元测试活动中,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。 在一种传统的结构化编程语言中,比如C,要进行测试的单元一般是函数或子过程。在象C++这样的面向对象的语言中, 要进行测试的基本单元是类。对Ada语言来说,开发人员可以选择是在独立的过程和函数,还是在Ada包的级别上进行单元测试。单元测试的原则同样被扩展到第四代语言(4GL)的开发中,在这里基本单元被典型地划分为一个菜单或显示界面。 单元测试不仅仅是作为无错编码一种辅助手段在一次 ...
  • 您正在使用Spring MVC Test的standaloneSetup运行您的测试,它使用最少的配置来启动和运行您的控制器。 该配置与运行整个应用程序时将使用的配置不同。 如果你想使用相同的配置,你可以使用webAppContextSetup : @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public cl ...
  • 好吧,经过一番摆弄,我得到了一个有效的解决方案。 我从phpunit.de下载了phpunit-lts.phar并将其直接放入项目中(以及其他文件...... meh)。 不要使用PHPUnit的最新版本(2014年3月21日的4.0)! IDEA或PHPStorm不支持3.7以上的版本(或看起来像3.8)。 你会得到 PHP Fatal error: Class IDE_PHPUnit_Framework_TestListener contains 1 abstract method and must ...
  • 你明显遇到的一个问题是你要求为Scala 2.11编译的scalatest版本,但你说你使用的是Scala 2.10。 你的build.sbt中需要“scalatest_2.10”而不是“scalatest_2.11”。 使用 libraryDependencies += "org.scalatest" % "scalatest_2.10" % "2.2.1" % "test" 或者甚至是 libraryDependencies += "org.scalatest" %% "scalatest" % "2 ...
  • 首先回答你的第二个问题,不,AOP本质上不与单元测试冲突。 通常我会说最好分别对方法和方面进行单元测试。 在您的情况下,有几个选项。 最简单的方法就是让单元测试设置方法确保线程具有所需的权限。 如果您不想这样做,有两种方法可以将单元测试分开。 第一种是将要应用安全性方面的方法中的所有代码提取到单独的方法中,如下所示: [SecurityAspect] void DoSomething() { DoSomethingInternal(); } void DoSomethingInternal() { ...