spring框架用什么软件
介绍
测试是企业软件开发的组成部分。 即使不是最重要的,它也与软件开发生命周期(SDLC)的任何其他阶段一样重要。 但是测试企业应用程序说起来容易做起来难。 有几个约束使项目中的测试工作成为主要挑战。 这些约束通常分为两类:框架相关的约束和项目方法相关的约束。
与框架相关的约束的一个示例是,J2EE体系结构模型没有将单元测试的方面作为软件开发的一部分加以考虑。 由于容器(应用程序服务器)是J2EE运行时体系结构的核心组件,因此很难在容器外部测试基于J2EE API构建的应用程序。 单元测试(在容器外部)对于实现高测试覆盖率至关重要。 重现许多故障场景也很容易,而无需应用程序服务器设置和代码部署的开销。 确保测试能够快速运行至关重要,这对于项目的开发或生产支持阶段至关重要。 使用单元测试来验证代码的任务可以最大程度地减少每次我们更改代码时等待部署应用程序所花费的非生产时间。
由于传统J2EE应用程序中的代码严重依赖于应用程序服务器,因此只有在J2EE容器中部署其功能时,才能对其功能进行全面测试。 但是,容器内测试速度太慢,并且对开发人员的生产率构成太多障碍,尤其是在项目规模较大且代码工件(Java源文件)数量很多的情况下。
有一些J2EE框架是从头开始构建的,为将软件测试集成到开发过程中提供了出色的支持。 Spring是此类Java企业应用程序开发框架之一。
最近,我为妻子在当地的一级汽车供应商公司工作的企业Java应用程序做过一些咨询。 该项目旨在创建一个用于跟踪其客户公司的配置文件的客户配置文件管理系统。 该应用程序的体系结构包括Hibernate 3.0,Spring 2.0和JBoss 4.0技术。 项目团队遵循敏捷软件开发方法,以在一周的迭代中交付需求。 他们使用Spring框架提供的集成测试功能来测试数据访问和服务层中的应用程序代码。 我们真的很喜欢Spring框架提供的测试支持。 它极大地简化了测试,并且使激进的为期一周的开发迭代成为可能且易于管理。
本文概述了Spring框架在单元测试和集成测试方面提供的支持。 我将使用样本贷款处理Web应用程序来帮助读者在典型的Java EE应用程序中实现敏捷测试框架,以及如何使用Spring测试类来测试应用程序功能。
敏捷软件测试
软件开发项目必须包括良好的设计和体系结构实践以及良好的测试实践。 一个应用程序可能具有非常好的体系结构,设计和代码,但是如果没有经过良好的测试,就不能认为它是成功的产品。 一些公司(软件服务供应商)因其产品质量而生死攸关,而测试对于这些公司的成功至关重要。
敏捷软件开发需要全面的测试策略,以实现软件开发项目中的敏捷性和质量。 敏捷测试包括单元测试和集成测试。 这意味着我们应该能够尽快执行测试(实现敏捷性的一种方法是在应用程序服务器外部运行测试)。 测试驱动开发( TDD )是敏捷开发过程的关键要素之一。 Spring和其他轻量级容器(例如PicoContainer和HiveMind)为测试驱动的软件开发提供了强大的支持。
让我们简要地看一下在典型的Java EE开发项目中进行单元和集成测试的重要性以及每种测试方法的目标和约束。
单元测试
单元测试用于测试应用程序中的特定单元(类)。 应该编写单元测试来测试类中的所有方法,包括方法中的所有异常路径。 单元测试的目的是能够快速测试任何新代码或对现有代码的更改,而不会造成诸如服务器配置,服务设置和应用程序部署等任务所涉及的开销和额外时间。 开发人员单元测试至关重要,因为在软件开发生命周期的早期(在编码和单元测试阶段)而不是在后期,更容易,更便宜地发现并修复错误。
JUnit是用于编写单元测试的流行测试框架。 在JUnit测试中,我们只需使用new
运算符实例化对象,而不必担心容器的JNDI资源和J2EE服务,例如资源池,JDBC连接池和JMS队列。 我们还可以使用Mock Objects之类的测试技术来隔离测试代码。 通过单元测试,无需为应用程序服务器甚至数据库服务器设置任何基础结构。
单元测试有一些限制。 单元测试不能解决应用程序功能需求的测试。 这些测试仅涵盖应用程序中每个模块的测试。 而且,我们无法测试诸如异步服务之类的场景,这些场景要求在应用程序服务器内部配置JMS消息队列。 但是,我们仍然应该能够对尽可能多的应用程序功能进行单元测试,并且将容器内测试仅用于那些无法在容器外部进行测试的功能。
整合测试
单元测试对于单独测试模块或类非常有用。 但是,对应用程序进行集成测试也很重要,以了解在集成环境中组装时各种模块如何协同工作。 与应用程序中的其他模块集成时,某些在模块级别正常运行的功能可能无法正常工作。 在敏捷开发环境中,不同的开发人员同时在应用程序的不同部分上工作,并且他们需要定期(在某些开发团队中每天)合并代码更改,这种情况非常现实。 集成测试包括测试应用程序的客户端和服务层之间的往返调用。 大多数集成测试通常在容器中运行。 但是要真正实现敏捷,我们将需要运行至少一些集成测试,而无需将任何代码部署到容器中。
集成测试在不能有效地对DAO接口的实现进行单元测试的DAO层中很有用。 集成测试的其他目标是测试方面,例如远程服务,状态(会话)管理,Web流和事务管理。 集成测试也有一些限制。 运行这些测试需要更长的时间。 由于需要将应用程序部署在Java EE容器中,因此运行这些测试还涉及服务器设置和配置开销。
应该注意的是,集成测试是补充测试,不能代替单元测试。 开发人员应首先为应用程序中的每个Java类编写足够的单元测试,以实现良好的代码覆盖率。 同时,应该编写足够多的集成测试,以涵盖应用程序中无法使用单元测试进行测试的不同用例场景。
除了单元测试和集成测试外,还有其他几种类型的测试。 下表列出了不同的测试策略及其目标。
表1. Java EE测试策略
测试策略 | 目的 |
---|---|
单元测试 | 在类级别测试应用程序以测试每个类中的所有方法。 |
模拟对象 | 在应用程序的客户端和服务层中使用模拟对象来测试类方法,而不必真正连接到后端数据库或其他网络资源。 |
ORM测试 | 验证在ORM层中定义的数据库表映射的完整性。 这些测试类使用数据库元数据信息来检查ORM映射。 |
数据库测试 | 隔离测试数据访问类(DAO)。 这些测试使每次测试运行的数据库表都处于已知状态。 |
XML测试 | 测试XML文档及其有效性,并比较两个不同的文档以断言它们是否相同。 |
整合测试 | 测试网站导航,Web流以及状态(会话)管理和事务管理。 |
回归测试 | 测试应用程序功能,就像最终用户在生产环境中部署该应用程序时会使用该应用程序一样。 这些测试通常由专门的质量检查团队使用诸如Mercury QuickTest Professional( QTP )之类的自动化测试工具来运行。 |
负载测试 | 测试应用程序的可伸缩性。 这些性能测试通常由专门的负载测试团队使用Mercury LoadRunner , WAPT和JMeter等工具进行 。 |
剖析 | 在应用程序运行时测试是否有内存泄漏,内存使用情况,垃圾回收等。 开发人员通过Java分析器(例如JProfiler ,Quest JProbe ,Eclipse测试和性能工具平台( TPTP ))运行应用程序。 |
有各种各样的开源测试框架可以执行上面列出的各种测试策略。 以下是其中一些框架的列表。
- JUnit的
- JMock
- ORM单元
- 数据库单元
- XMLUnit
- JUnitEE
- 模拟EJB
- 仙人掌
由于测试方面决定了项目的成败,因此,我们用于软件开发的任何Java EE框架都应为将测试无缝集成到设计和开发阶段提供支持。 让我们从单元测试和集成测试的角度来看一个理想的Java EE框架应该具有的一些特征。
敏捷开发:
该框架应有助于应用程序的迭代和敏捷软件开发。 越来越多的开发团队正在采用敏捷方法,而敏捷测试和早期反馈是迭代开发的主要部分。
测试驱动开发:
一个公认的事实是,从应用程序开发生命周期的早期阶段就需要立即解决测试问题。 在此过程的早期发现并修复错误更便宜,更有效。 查找任何错误的最佳方法是在项目的每个迭代的设计和开发阶段中“尽早”进行测试。
基于界面的设计:
我们面向对象的程序员努力争取的最佳实践之一是将Java类编写为接口而不是具体类。 编写接口使我们在运行单元和集成测试时具有极大的灵活性,而不必在更改服务组件的实现时修改客户端代码。
关注点分离:
当我们根据我们要解决的特定应用关注点(例如域,业务逻辑,数据访问和基础结构逻辑)有意识地在单独的模块中设计和编写代码时,我们实现了“关注点分离”(SOC)。 这样,就可以隔离测试诸如日志记录,异常处理和应用程序安全性之类的不同问题,而无需依赖于其他模块。
分层架构:
典型的Java企业应用程序的设计方式是存在客户端,服务,域和持久性层。 层中的任何元素都应仅依赖于同一层中的其他元素,或仅依赖于该层“之下”的层中的元素(假设表示是体系结构层中的最顶层,而持久性是体系结构层中的最底层)。 这意味着客户端层只能依赖服务层。 服务层只能依赖于域层,而域层只能依赖于持久层。 Java EE框架应支持所有这些层中的单元和集成测试,而不依赖于其他层。
非侵入式:
诸如EJB和Struts之类的框架迫使开发人员在应用程序中扩展框架特定的类(例如EJBObject , ActionForm , Action等)。 这会导致对特定框架的应用程序依赖性,这使得单元测试成为一个挑战,并且当我们需要切换到其他(更好的)框架时,还会导致额外的工作。 这些框架本质上是侵入性的,应在考虑将来可扩展性要求的情况下谨慎选择。 请注意,作为EJB(Java EE 5)的一部分的最新版本的EJB规范(版本3.0)的侵入性较小,因为Entities(以前称为Entity Bean)和Session Bean是纯Java类,可以在容器外部进行测试,类似于Spring。豆子。
控制反转(IoC):
该框架应为在应用程序中创建的对象上的控制反转提供支持。 控制反转或IoC(也称为依赖注入, DI )设计模式为集成测试带来了许多好处。 主要好处是,基于IoC模式设计的应用程序对容器的依赖程度远小于使用传统J2EE应用程序体系结构创建的应用程序。
面向方面的编程(AOP):
AOP使行为(否则会分散在不同的类中)可以集中在单个模块中。 它在单元和集成测试中非常有用,在这里我们可以使用Aspects声明性地测试Java EE服务,例如事务管理和基于角色的安全性。
面向服务的体系结构:
测试是SOA基础结构中的关键组件,因为服务用于企业中的不同模块和应用程序。 如果未完全测试服务组件的特定用例,则在生产环境中实施代码更改时,可能会导致生产问题和质量问题。
数据访问抽象:
一致的数据访问架构方法对于测试应用程序的数据访问功能也非常重要。 数据访问抽象应该与任何持久性实现框架(例如Hibernate , JPA , JDO , iBATIS , OJB和Spring JDBC )无关。 它还应该很好地处理在持久层中引发的数据访问异常。
交易管理:
该框架应提供用于测试事务管理的抽象接口。 它应该与JDBC和JTA事务(用于Container和Bean管理的事务)以及其他事务对象(例如Hibernate Transaction )集成。
弹簧测试支持
Spring框架是根据敏捷测试策略设计的,旨在帮助开发人员遵循合理的设计和有效的单元测试最佳实践。 它还为在应用程序服务器外部运行集成测试提供了强大的支持。 在我们使用Spring时,Spring是一个非侵入性的框架,在该框架上应用程序代码的依赖性最小。 我们可以将应用程序对象配置为纯Java类(POJO),而不必扩展任何特定于Spring的类(注意:当您使用JDBCTemplate , JNDITemplate , HibernateDaoSupport等Spring模板帮助器类时,您将在Spring框架上添加依赖项)。 我们甚至可以配置在Spring存在之前编写的所有旧类。
总体而言,Spring框架特别是Spring Testing模块支持以下方面:
隔离:
Spring通过注入模拟实现,使J2EE开发人员可以灵活地隔离地测试Java类。 例如,我们可以使用对应的Repository类的模拟实现来测试服务类。 这样,我们可以在服务类中测试业务逻辑,而不必担心连接数据库的持久性细节。
控制反转:
该框架为POJO提供了复杂的配置管理。 Spring IoC容器可以管理细粒度或粗粒度的Java类。 它使用bean工厂来实例化应用程序对象,并使用构造函数或setter注入机制将它们连接在一起。
资料存取:
它为数据访问提供了很好的持久性体系结构,并为数据访问异常提供了良好的层次结构。 它提供了一些辅助类(例如JdbcTemplate , HibernateTemplate , TopLinkTemplate和JpaTemplate )来与领先的持久性框架一起使用。
交易管理:
Spring提供了一个很好的抽象框架来管理事务(本地和全局)。 这种抽象在广泛的开发环境中提供了一致的编程模型,并且是Spring的声明式和程序化事务管理的基础。
使用Spring进行集成测试
Spring配置,依赖项注入(DI),数据访问(CRUD)和事务管理是我们可以使用Spring Testing框架在服务器环境之外进行测试的一些问题。 数据访问测试是针对真实数据库执行的,因此无需在这些测试中使用任何模拟对象。
在中小型Web应用程序中,Spring上下文加载时间可能不是问题。 但是对于大型企业应用程序,可能需要花费大量时间来实例化应用程序中的类。 而且,在每个测试夹具中运行每个测试用例的开销会导致整体测试运行速度变慢,并不利地影响开发人员的生产率。 牢记这些问题,Spring开发团队编写了一些测试类,可用作在容器外部运行的集成测试。 由于这些测试类都是基于JUnit API的扩展,我们得到的JUnit的所有好处开箱的时候,我们使用Spring测试类。 这些测试类为每种测试方法设置事务,并自动清除(在每种方法结束时回滚事务),从而无需执行任何数据库设置和拆卸任务。
以下是在Spring应用程序中运行集成测试时可以验证的项目列表:
- 通过在每个测试用例的执行之间缓存已加载的上下文,来加载Spring上下文和管理上下文。 另外,通过Spring IoC容器验证应用程序上下文的正确连接。
- 测试夹具和Spring配置详细信息的依赖注入(以验证是否正确加载了特定的数据访问(存储库)类配置)。
- 用于数据访问和CRUD操作的便捷变量(用于测试数据库选择和更新的数据访问类的逻辑)。
- 交易管理。
- ORM映射文件配置(验证是否与持久性对象相关的所有内容均正确映射,并且正确的延迟加载语义是否正确)。
我们可以像JUnit测试一样运行集成测试。 与单元测试相比,它们的运行速度较慢,因为我们在集成级别而不是仅在类级别测试代码。 但是,这些集成测试的执行速度要比使用容器内测试框架(如JUnitEE或Cactus)创建的测试要快得多,这些框架依赖于在执行测试之前将应用程序部署到容器上。
Spring集成测试类旨在解决各种测试问题,因此org.springframework.test包中有不同的测试类。 下表显示了Spring框架中提供的一些用于集成测试的测试类,以及可以在哪些情况下使用它们。
表2.弹簧测试类
测试类别名称 | 描述 |
---|---|
AbstractDependencyInjection SpringContextTests | 这个测试类注入了测试依赖项,因此我们不需要专门执行Spring应用程序上下文查找。 它还会自动在getConfigLocations()方法指定的配置文件集中找到相应的对象。 |
AbstractTransactionalDataSource SpringContextTests | 该测试类用于测试在事务内运行的代码。 它为每个测试用例创建并回滚事务。 我们在假设存在事务的情况下编写代码。 它提供了诸如JdbcTemplate之类的字段,可用于验证测试操作后的数据库状态,或验证由应用程序代码执行的查询的结果。 ApplicationContext也被继承,并且可以在必要时用于显式查找。 |
AbstractJpaTests | 该测试类用于测试JPA功能。 它提供了一个EntityManager实例,可用于调用JPA方法。 |
AbstractAspectjJpaTests | 该类从AbstractJpaTests扩展而来,用于使用AspectJ进行加载时编织(LTW)的目的。 我们重写方法getActualAopXmlLocation()以指定AspectJ的配置xml文件的位置。 |
AbstractModelAndViewTests | 这是一个方便的基类,用于在应用程序中测试表示层和控制器层(使用Spring MVC)。 |
下面的图1显示了从JUnit TestCase类扩展的Spring框架测试类的类层次结构图。 注意:这些测试类是spring-mock.jar文件的一部分(位于Spring框架安装目录中的dist文件夹中)。
图1. Spring测试类的层次结构(单击屏幕快照打开一个完整的视图。)
以下是决定从哪个测试类扩展时要考虑的因素列表:
- 使用Spring应用程序上下文而不必编写代码来初始化和管理它。
- 测试数据访问(使用数据源)
- 测试事务内部的方法(使用transactionmanager)
- JDK版本:如果使用的是JDK 1.4,则无法利用JDK 1.5中引入的注释。
下一节将提供这些测试类的更多详细信息。
AbstractSpringContextTests:
这是所有Spring测试类的基类。 它提供了方便的方法来加载Spring应用程序上下文。 当我们需要测试Spring上下文的加载而不必显式管理依赖项注入时,我们可以扩展此类。 它通过键维护上下文的静态缓存,如果应用程序具有多个Spring Bean(尤其是用于与ORM工具(例如Hibernate)一起使用的Bean,例如LocalSessionFactoryBean等)进行加载,则有助于显着提高性能。 因此,在大多数情况下,一次初始化应用程序上下文并将其缓存以供后续查找是有意义的。
AbstractSingleSpringContextTests:
这是一个抽象测试类,仅公开一个Spring ApplicationContext 。 它将基于上下文密钥缓存应用程序上下文,上下文密钥通常是描述Spring资源描述符的配置位置(String [])。 它封装了与加载和管理Spring上下文有关的所有功能。
AbstractDependencyInjectionSpringContextTests:
这是一个方便的超类,用于依赖于Spring应用程序上下文的测试。 它具有setAutowireMode()方法,该方法用于为依赖注入设置的测试属性设置自动装配模式。 默认值为AUTOWIRE_BY_TYPE,但也可以将其设置为AUTOWIRE_BY_NAME或AUTOWIRE_NO 。
AbstractTransactionalSpringContextTests:
此类具有几种方便的方法,可以在运行集成测试时使事务管理任务更轻松。 它提供了事务变量transactionManager , transactionDefinition和transactionStatus,用于管理测试方法中的事务。 它还有一个名为endTransaction()的方法来强制事务的提交或回滚。 startNewTransaction()方法用于启动一个新事务,该事务在调用endTransaction()之后被调用。
AbstractTransactionalDataSourceSpringContextTests:
这是最常用的Spring测试类之一。 它提供了有用的继承的受保护字段,例如JdbcTemplate ,可用于在数据库上运行CRUD操作。 它还在自己的事务中执行每个测试方法,默认情况下会自动回滚。 这意味着即使测试更改了数据库状态(使用插入,更新或删除),也不需要拆卸或清除脚本,因为数据库更改会自动回滚。 此类中的其他帮助方法是countRowsInTable() ,这是一种方便的方法,用于检查是否已添加新记录或已删除现有记录, deleteFromTables用于删除表中的所有行, executeSqlScript用于执行表中的所有操作。给定SQL脚本(这些数据库更改将根据当前事务的状态回滚)。
AbstractAnnotationAwareTransactionalTests:
该测试类公开SimpleJdbcTemplate变量。 使用此类,我们可以使用@Transactional批注控制交易行为。 我们还可以使用@NotTransactional来阻止创建任何事务(请注意,这两个是Spring特定的注释,它们创建了对Spring框架的依赖。只有在使用JDK 1.5时,才可以使用此功能。
AbstractJpaTests:
这是基础测试类,是从我们要测试使用JPA API编写的DAO类时扩展的。 它为持久化方法公开了EntityManagerFactory和共享的EntityManager 。 它需要注入DataSource和JpaTransactionManager 。
一旦确定要扩展哪个测试类,以下是将Spring测试类集成到集成测试中所需的步骤:
- 扩展Spring测试类之一(如果您特别想测试JPA功能,则通常为AbstractTransactionalDataSourceSpringContextTests或AbstractJpaTests )。 请注意,JPA仅是Java SE 5.0,因此,如果您在应用程序中使用JDK1.5之前的版本,则无法扩展此类。
- 重写getConfigLocations()方法以加载数据源,事务管理器和应用程序代码中使用的其他资源的所有配置文件。 使用@Override批注指定我们将覆盖在超类( AbstractSingleSpringContextTests )中定义的此方法。
- 为测试类中使用的所有DAO对象编写设置方法(这些DAO对象由Spring IoC容器使用指定的自动装配选项注入)。
- 使用这些DAO对象(依次使用数据源),事务管理器和ORM持久性帮助器方法添加测试方法。
Rod Johnson关于使用Spring进行系统测试的演示文稿是Spring Test API在单元和集成测试中提供的支持的绝佳资源。
样品申请
本文中使用的示例应用程序是房屋贷款处理系统。 用例是在系统中处理抵押贷款的资金。 当贷款申请提交给抵押贷款公司时,首先要经过承销过程,承销商会根据客户的收入详细信息,信用记录和其他一些因素批准或拒绝贷款请求。 如果贷款申请获得批准,则将完成结算和融资流程。
贷款处理应用程序中的资金模块使资金的分配过程自动化。 融资过程通常从贷方公司将贷款包转发给产权公司开始。 然后,产权公司审查贷款计划,并安排完成贷款的日期和时间。 借款人和卖方在产权公司与结业代理会面。
在结账期间,买方(借款人)和卖方阅读并签署了最终的贷款单据。 借款人将支付与支付贷款有关的任何费用的首付款和钱。 另外,在关闭过程中需要支付关闭费用和代管费用。 闭幕会议之后,产权公司将签署的协议发送给贷方以供资金。 贷方将资金转移到产权公司。
应用架构:
在示例应用程序的设计中,我遵循了流行的分层体系结构。 这些层是表示层,应用程序(控制器),服务,域和数据访问层。 我按照域驱动设计原则中推荐的命名约定将数据访问类(DAO)命名为存储库。 我将服务层,域层和数据访问层编写为纯Java类。 我不会在本文中介绍Presentation和Application层类。
LoanApp Web应用程序的应用程序体系结构图如下图2所示。
图2. LoanApp应用程序体系结构图(单击屏幕快照以打开完整尺寸的视图。)
资料模型:
我使用HSQL数据库为loanapp应用程序创建了贷款应用程序数据库(称为LoanDB )。 出于演示目的,我使用名为LOANDETAILS , FUNDINGDETAILS和BORROWERDETAILS的 3个表来简化数据模型。
领域模型:
该模型中有三个域对象,分别是LoanDetails , BorrowerDetails和FundingDetails,以捕获贷款处理系统的资金模块中所需的业务实体。
注意:示例应用程序中使用的模型仅用于演示目的。 实际的应用程序域模型将比此处描述的更为复杂。
我为每个域类编写了数据访问类。 DAO类是LoanDetailsRepositoryJpaImpl , BorrowerDetailsRepositoryJpaImpl和FundingDetailsRepositoryJpaImpl 。 还有一个名为FundingServiceImpl的服务类,该服务类封装了处理资金请求的逻辑。 它调用DAO,以批准,拒绝或取消指定贷款的资金请求。
持续性:
我使用Java Persistence API (JPA)作为Web应用程序中对象关系映射(ORM)要求的持久性模型。 Spring提供了对所有领先的持久性框架的支持,例如Hibernate , JDO , iBATIS , TopLink和JPA 。 Spring JPA是Spring 2.0版本的一部分,在org.springframework.orm.jpa软件包中包括JPA帮助器类。 我将EntityManager选项(而不是JPA模板)用于持久性方面。 这种方法不依赖于Spring,仍然可以使用Spring应用程序上下文进行管理。 我们还可以利用注释使用@PersistenceContext和@PersistenceUnit标签注入EntityManager 。
下表显示了示例应用程序中使用的框架和技术。
表3.示例应用程序中使用的技术
层 | 技术 | 版 |
---|---|---|
控制者 | SpringMVC | 2.0 |
服务 | 弹簧 | 2.0 |
域 | 纯Java类 | |
坚持不懈 | JPA | |
数据库 | HSQLDB服务器 | 1.8.0 |
Java | Java SE | 6.0 |
建立 | 蚂蚁 | 1.7 |
集成开发环境 | 蚀 | 3.3 |
我在示例应用程序中使用的其他工具是用于静态代码分析的Metrics和FindBugs ,用于代码覆盖率的EclEmma 。
测试中
这里快速重申了我们在编写和执行测试中的目标。
- 我们希望在不离开IDE(Eclipse)的情况下编写代码并运行测试。
- 不应对所需的代码进行特殊部署
- 我们应该能够直接在IDE中利用其他代码分析工具,例如Metrics和FindBugs,这样我们就可以立即发现任何错误并修复这些问题。
除了对应用程序中的每个Repository类(即LoanDetailsRepositoryJpaImpl , BorrowerDetailsRepositoryJpaImpl和FundingDetailsRepositoryJpaImpl )进行传统的单元测试(使用JUnit) 之外 ,我还编写了集成测试以验证FundingServiceImpl类方法。
下表显示了主要和相应测试类别的列表。
表4:贷款申请中的测试案例列表
应用层 | 主班 | 测试班 |
---|---|---|
资料存取 | 贷款详细信息存储库Impl | LoanDetailsRepositoryJpaImplTest, LoanDetailsRepositoryJpaImplSpringDITest |
资料存取 | 借款人详细信息存储库 | BorrowerDetailsRepositoryJpaImplTest |
资料存取 | FundingDetailsRepositoryImpl | FundingDetailsRepositoryJpaImplTest |
服务 | FundingServiceImpl | FundingServiceImplIntegrationTest, FundingServiceImplSpringDITest, FundingServiceImplSpringJPATest |
为了比较Spring的集成测试支持,我首先编写了不使用Spring测试类( FundingServiceImplTest )的资金服务集成测试。 然后,我编写了另外两个测试类( FundingServiceImplSpringDITest和FundingServiceImplSpringJpaTest )来测试FundingServiceImpl类中的逻辑,但这一次使用Spring测试类。 我使用了一些帮助程序变量和方法来帮助运行数据库查询。 这些是变量jdbcTemplate , simpleJdbcTemplate , sharedEntityManager以及方法countRowsInTable() , deleteFromTables()和endTransaction() 。
让我们看一下这些单元和集成测试类的代码示例,以查看Spring Testing API自动执行了多少样板测试任务,以便开发人员可以专注于声明实际的业务逻辑。
首先,让我们看一下Spring配置XML文件。 该文件具有示例应用程序中使用的针对存储库(DAO)类的Spring bean定义。 清单1显示了loanApplicationContext-jpa.xml配置文件的代码。
清单1. LoanApp Spring配置详细信息
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
<!--
! Load JDBC Properties
!-->
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:jdbc.properties"/>
</bean>
<!--
! Service classes
!-->
<bean id="fundingService" class="com.ideature.agiletestingspring.loanapp.service.FundingServiceImpl" >
<property name="loanDetailsRepository" ref="loanDetailsRepository"/>
<property name="borrowerDetailsRepository" ref="borrowerDetailsRepository"/>
<property name="fundingDetailsRepository" ref="fundingDetailsRepository"/>
</bean>
<!--
! Repository classes
!-->
<bean id="loanDetailsRepository" class="com.ideature.agiletestingspring.loanapp.repository.LoanDetailsRepositoryJpaImpl" />
<bean id="borrowerDetailsRepository" class="com.ideature.agiletestingspring.loanapp.repository.BorrowerDetailsRepositoryJpaImpl" />
<bean id="fundingDetailsRepository" class="com.ideature.agiletestingspring.loanapp.repository.FundingDetailsRepositoryJpaImpl" />
<!--
! Configure the JDBC datasource. Use the in-container datasource
! (retrieved via JNDI) in the production environment.
!-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!--
! Configure the entity manager.
!-->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitName" value="LoanDBSpring"/>
<property name="dataSource" ref="dataSource"/>
<property name="loadTimeWeaver">
<!-- InstrumentationLoadTimeWeaver expects you to start the appserver with
-javaagent:/Java/workspace2/spring/dist/weavers/spring-agent.jar
-->
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
</property>
<!--
! JPA Adapter
!-->
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
<property name="databasePlatform"
value="oracle.toplink.essentials.platform.database.HSQLPlatform"/>
<property name="generateDdl" value="false"/>
<property name="showSql" value="true" />
</bean>
<!--
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="databasePlatform" value="org.hibernate.dialect.HSQLDialect" />
<property name="generateDdl" value="true" />
<property name="showSql" value="true" />
</bean>
-->
</property>
</bean>
<!--
! Transaction manager for EntityManagerFactory.
!-->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
<property name="dataSource" ref="dataSource"/> </bean>
<!--
! Use Spring's declarative @Transaction management !-->
<tx:annotation-driven/>
<!--
! Configure to make Spring perform persistence injection using
! @PersistenceContext/@PersitenceUnit annotations
!-->
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>
</beans>
我编写了两个基类,示例应用程序中的所有测试类都从中扩展。 这些是BaseDataSourceSpringContextIntegrationTest和BaseJpaIntegrationTest 。
BaseDataSourceSpringContextIntegrationTest:
这是用于测试数据访问和Spring上下文加载功能的基础测试类。 它扩展了Spring的AbstractTransactionalDataSourceSpringContextTests类。 它通过调用getConfigLocations()方法加载应用程序上下文。 清单2显示了此抽象测试类的源代码。
清单2. BaseDataSourceSpringContextIntegrationTest基本测试类
package com.ideature.agiletestingspring.loanapp;
import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
public abstract class BaseDataSourceSpringContextIntegrationTest extends AbstractTransactionalDataSourceSpringContextTests {
private static final String[] configFiles = new String[]{"loanapp-applicationContext-jpa.xml"};
@Override
protected String[] getConfigLocations() {
return configFiles;
}
}
BaseJpaIntegrationTest:
这是为使用JPA测试ORM功能而创建的所有集成测试的基类。 它扩展了Spring的AbstractJpaTests类。 下面的清单3显示了BaseJpaIntegrationTest class.
的代码BaseJpaIntegrationTest class.
清单3. BaseJpaIntegrationTest测试类
package com.ideature.agiletestingspring.loanapp;
import org.springframework.test.jpa.AbstractJpaTests;
public class BaseJpaIntegrationTest extends AbstractJpaTests {
private static final String[] configFiles = new String[]{"loanapp-applicationContext-jpa.xml"};
@Override
protected String[] getConfigLocations() {
return configFiles;
}
}
LoanApp应用程序中其他测试类的详细信息如下:
LoanDetailsRepositoryJpaImplTest:
这是普通的香草存储库单元测试类,用于测试LoanDetailsRepositoryJpaImpl类中的CRUD逻辑。 它显式初始化Spring应用程序上下文,从上下文中检索loanDetailsRepository ,然后在存储库类中调用CRUD方法。 它还调用delete方法以删除添加在LOANDETAILS表中的新记录。 该测试类还具有setUp()和tearDown()方法,以初始化和清除测试方法中使用的资源。
LoanDetailsRepositoryJpaImplSpringDITest:
这个测试类类似于LoanDetailsRepositoryJpaImplTest,但是它使用Spring测试类使测试LoanDetailsRepository类中的数据访问方法变得非常容易。 它扩展了BaseDataSourceSpringContextIntegrationTest 。 它具有setLoanDetailsRepository()的setter方法,因此Spring的IoC容器将在运行时注入存储库接口的正确实现。 没有样板代码,如初始化应用程序上下文或setUp()和tearDown()方法。 另外,由于所有数据库更改都将在每个测试方法结束时自动回滚,因此无需调用delete方法。 我们使用AUTOWIRE_BY_TYPE (默认选项)通过setLoanDetailsRepository()方法自动关联 LoanDetailsRepository 。
FundingServiceImplIntegrationTest:
这是FundingServiceImpl类的测试类。 它显示了如果不利用Spring测试类,我们必须编写多少代码。 清单4显示了此集成测试类的代码。
清单4. FundingServiceImplIntegrationTest的示例代码
package com.ideature.agiletestingspring.loanapp.service;
import static org.junit.Assert.assertEquals;
import java.util.Collection;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.ideature.agiletestingspring.loanapp.LoanAppConstants;
import com.ideature.agiletestingspring.loanapp.LoanAppException;
import com.ideature.agiletestingspring.loanapp.domain.BorrowerDetails;
import com.ideature.agiletestingspring.loanapp.domain.FundingDetails;
import com.ideature.agiletestingspring.loanapp.domain.LoanDetails;
import com.ideature.agiletestingspring.loanapp.dto.FundingDTO;
import com.ideature.agiletestingspring.loanapp.repository.BorrowerDetailsRepository;
import com.ideature.agiletestingspring.loanapp.repository.FundingDetailsRepository;
import com.ideature.agiletestingspring.loanapp.repository.LoanDetailsRepository;
import com.ideature.agiletestingspring.loanapp.repository.RepositoryException;
public class FundingServiceImplIntegrationTest {
private static final Log log = LogFactory.getLog(FundingServiceImplIntegrationTest.class);
private static final String[] configFiles = new String[] {
"loanapp-applicationContext-jpa.xml"};
private ApplicationContext ctx = null;
private LoanDetailsRepository loanDetailsRepository = null;
private BorrowerDetailsRepository borrowerDetailsRepository = null;
private FundingDetailsRepository fundingDetailsRepository = null;
private FundingService fundingService;
@Before
public void setUp() {
ctx = new ClassPathXmlApplicationContext(configFiles);
log.debug("ctx: "+ctx);
loanDetailsRepository = (LoanDetailsRepository)ctx.getBean("loanDetailsRepository");
borrowerDetailsRepository = (BorrowerDetailsRepository)ctx.getBean("borrowerDetailsRepository");
fundingDetailsRepository = (FundingDetailsRepository)ctx.getBean("fundingDetailsRepository");
log.debug("loanDetailsRepository: "+loanDetailsRepository);
fundingService = (FundingService)ctx.getBean("fundingService");
log.debug("fundingService: " + fundingService);
}
@After
public void tearDown() {
fundingService = null;
loanDetailsRepository = null;
borrowerDetailsRepository = null;
fundingDetailsRepository = null;
ctx = null;
log.debug("ctx set null.");
}
@Test
public void testLoanFunding() {
// -------------------------------------------
// Set LOAN details
// -------------------------------------------
long loanId = 100;
LoanDetails loanDetails = new LoanDetails();
loanDetails.setLoanId(loanId);
loanDetails.setLoanAmount(450000);
loanDetails.setLoanStatus("REQUESTED");
loanDetails.setProductGroup("FIXED");
loanDetails.setProductId(1234);
loanDetails.setPurchasePrice(500000);
// -------------------------------------------
// Set BORROWER details
// -------------------------------------------
BorrowerDetails borrowerDetails = new BorrowerDetails();
long borrowerId = 131;
borrowerDetails.setBorrowerId(borrowerId);
borrowerDetails.setFirstName("BOB");
borrowerDetails.setLastName("SMITH");
borrowerDetails.setPhoneNumber("123-456-7890");
borrowerDetails.setEmailAddress("test.borr@abc.com");
borrowerDetails.setLoanId(loanId);
// -------------------------------------------
// Set FUNDING details
// -------------------------------------------
long fundingTxnId = 300;
FundingDetails fundingDetails = new FundingDetails();
fundingDetails.setFundingTxnId(fundingTxnId);
fundingDetails.setLoanId(loanId);
fundingDetails.setFirstPaymentDate(new Date());
fundingDetails.setFundType(LoanAppConstants.FUND_TYPE_WIRE);
fundingDetails.setLoanAmount(450000);
fundingDetails.setMonthlyPayment(2500);
fundingDetails.setTermInMonths(360);
// Populate the DTO object
FundingDTO fundingDTO = new FundingDTO();
fundingDTO.setLoanDetails(loanDetails);
fundingDTO.setBorrowerDetails(borrowerDetails);
fundingDTO.setFundingDetails(fundingDetails);
try {
Collection
loans = loanDetailsRepository.getLoans();
log.debug("loans: " + loans.size());
// At this time, there shouldn't be any loan records
assertEquals(0, loans.size());
Collection
borrowers = borrowerDetailsRepository.getBorrowers();
log.debug("borrowers: " + borrowers.size());
// There shouldn't be any borrower records either
assertEquals(0, borrowers.size());
Collection
fundingDetailsList = fundingDetailsRepository.getFundingDetails();
log.debug("FundingDetails: " + fundingDetailsList.size());
// There shouldn't be any fundingDetails records
assertEquals(0, fundingDetailsList.size());
// Call service method now
fundingService.processLoanFunding(fundingDTO);
// Assert that the new record has been saved to the DB.
loans = loanDetailsRepository.getLoans();
log.debug("After adding a new record - loans 2: " + loans.size());
// Now, there should be one loan record
assertEquals(1, loans.size());
borrowers = borrowerDetailsRepository.getBorrowers();
log.debug("After adding a new record - borrowers2: " + borrowers.size());
// Same with borrower record
assertEquals(1, borrowers.size());
fundingDetailsList = fundingDetailsRepository.getFundingDetails();
log.debug("After adding a new record - # of records: " + fundingDetailsList.size());
// Same with funding details record
assertEquals(1, fundingDetailsList.size());
// Now, delete the newly added records
// Delete the funding details record
fundingDetailsRepository.deleteFundingDetails(fundingTxnId);
// Delete the borrower details record
borrowerDetailsRepository.deleteBorrower(borrowerId);
// Delete loan details record last
loanDetailsRepository.deleteLoanDetails(loanId);
} catch (RepositoryException re) {
log.error("RepositoryException in testLoanFunding() method.", re);
} catch (LoanAppException lae) {
log.error("LoanAppException in testLoanFunding() method.", lae);
}
}
}
正如您在testLoanFunding()方法中看到的那样 ,我们必须在FundingDetailsRepository类中显式调用delete方法,以保持数据库状态与运行此测试之前的状态相同。
FundingServiceImplSpringDITest:
此类扩展了BaseDataSourceSpringContextIntegrationTest基类。 它具有Repository对象的setter方法,因此在加载应用程序上下文时,这些方法将由Spring DI容器注入。 清单5中显示了此集成测试类的源代码。
清单5. FundingServiceImplSpringDITest测试类
package com.ideature.agiletestingspring.loanapp.service;
import java.util.Collection;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.internal.runners.TestClassRunner;
import org.junit.runner.RunWith;
import com.ideature.agiletestingspring.loanapp.BaseDataSourceSpringContextIntegrationTest;
import com.ideature.agiletestingspring.loanapp.LoanAppConstants;
import com.ideature.agiletestingspring.loanapp.domain.LoanDetails;
import com.ideature.agiletestingspring.loanapp.repository.LoanDetailsRepository;
import com.ideature.agiletestingspring.loanapp.repository.RepositoryException;
@RunWith(TestClassRunner.class)
public class FundingServiceImplSpringDITest extends BaseDataSourceSpringContextIntegrationTest {
private static final Log log = LogFactory.getLog(FundingServiceImplSpringDITest.class);
private LoanDetailsRepository loanDetailsRepository = null;
public void setLoanDetailsRepository(LoanDetailsRepository loanDetailsRepository) {
this.loanDetailsRepository = loanDetailsRepository;
}
@Before
public void initialize() throws Exception {
super.setUp();
}
@After
public void cleanup() throws Exception {
super.tearDown();
}
@Test
public void testFindLoans() throws RepositoryException {
// First delete all the records from LoanDetails table
// by calling deleteFromTables() helper method.
deleteFromTables(new String[]{"LoanDetails"});
Collection
loans = loanDetailsRepository.getLoans();
assertEquals(0, loans.size());
}
@Test
public void testJdbcQueryUsingJdbcTemplate() {
// Use jdbcTemplate to get the loan count
int rowCount = jdbcTemplate.queryForInt("SELECT COUNT(0) from LoanDetails");
assertEquals(rowCount,0);
}
@Test
public void testLoadLoanDetails() throws RepositoryException {
int rowCount = countRowsInTable("LOANDETAILS");
log.info("rowCount: " + rowCount);
long loanId = 100;
double loanAmount = 450000.0;
String loanStatus = LoanAppConstants.STATUS_REQUESTED;
String productGroup = "FIXED";
long productId = 1234;
double purchasePrice = 500000.0;
// Add a new record
LoanDetails newLoan = new LoanDetails();
newLoan.setLoanId(loanId);
newLoan.setLoanAmount(loanAmount);
newLoan.setLoanStatus(loanStatus);
newLoan.setProductGroup(productGroup);
newLoan.setProductId(productId);
newLoan.setPurchasePrice(purchasePrice);
// Insert a new record using jdbcTemplate helper attribute
jdbcTemplate.update("insert into LoanDetails (LoanId,ProductGroup,ProductId,LoanAmount,PurchasePrice," +
"PropertyAddress,LoanStatus) values (?,?,?,?,?,?,?)",
new Object[] { new Long(newLoan.getLoanId()),newLoan.getProductGroup(),new Long(newLoan.getProductId()),
new Double(newLoan.getLoanAmount()), new Double(newLoan.getPurchasePrice()),"123 MAIN STREET","IN REVIEW" });
// Explicitly end the transaction so the new record will be
// saved in the database table.
endTransaction();
// Start a new transaction to get a different unit of work (UOW)
startNewTransaction();
rowCount = countRowsInTable("LOANDETAILS");
log.info("rowCount: " + rowCount);
LoanDetails loanDetails1 = loanDetailsRepository.loadLoanDetails(loanId);
// We should get a null as the return value.
assertNull(loanDetails1);
}
@Test
public void testInsertLoanDetails() throws RepositoryException {
int loanCount = 0;
Collection
loans = loanDetailsRepository.getLoans();
loanCount = loans.size();
assertTrue(loanCount==0);
long loanId = 200;
LoanDetails loanDetails = loanDetailsRepository.loadLoanDetails(loanId);
assertNull(loanDetails);
double loanAmount = 600000.0;
String loanStatus = LoanAppConstants.STATUS_IN_REVIEW;
String productGroup = "ARM";
long productId = 2345;
double purchasePrice = 700000.0;
// Add a new record
LoanDetails newLoan = new LoanDetails();
newLoan.setLoanId(loanId);
newLoan.setLoanAmount(loanAmount);
newLoan.setLoanStatus(loanStatus);
newLoan.setProductGroup(productGroup);
newLoan.setProductId(productId);
newLoan.setPurchasePrice(purchasePrice);
loanDetailsRepository.insertLoanDetails(newLoan);
loans = loanDetailsRepository.getLoans();
log.info("loans.size(): " + loans.size());
System.out.println("loans.size(): " + loans.size());
assertEquals(loanCount + 1, loans.size());
}
@Test
public void testUpdateLoanDetails() throws Exception {
// First, insert a new record
long loanId = 100;
double loanAmount = 450000.0;
String oldStatus = LoanAppConstants.STATUS_FUNDING_COMPLETE;
String productGroup = "FIXED";
long productId = 1234;
double purchasePrice = 500000.0;
String propertyAddress = "123 MAIN STREET";
// Add a new record
LoanDetails newLoan = new LoanDetails();
newLoan.setLoanId(loanId);
newLoan.setLoanAmount(loanAmount);
newLoan.setLoanStatus(oldStatus);
newLoan.setProductGroup(productGroup); newLoan.setProductId(productId);
newLoan.setPurchasePrice(purchasePrice);
newLoan.setPropertyAddress(propertyAddress);
// Insert a new record using jdbcTemplate helper attribute
jdbcTemplate.update("insert into LoanDetails (LoanId,ProductGroup,ProductId,LoanAmount,PurchasePrice," +
"PropertyAddress,LoanStatus) values (?,?,?,?,?,?,?)",
new Object[] { new Long(newLoan.getLoanId()),newLoan.getProductGroup(),new Long(newLoan.getProductId()),
new Double(newLoan.getLoanAmount()), new Double(newLoan.getPurchasePrice()),newLoan.getPropertyAddress(),
newLoan.getLoanStatus() });
LoanDetails loanDetails1 = loanDetailsRepository.loadLoanDetails(loanId);
String status = loanDetails1.getLoanStatus();
assertEquals(status, oldStatus);
String newStatus = LoanAppConstants.STATUS_FUNDING_DENIED;
// Update status field
loanDetails1.setLoanStatus(newStatus);
loanDetailsRepository.updateLoanDetails(loanDetails1);
status = loanDetails1.getLoanStatus();
assertEquals(status, newStatus);
}
}
此类中使用了辅助方法deleteFromTables()从FUNDINGDETAILS表中删除数据。 从Spring测试超类可以使用此方法。 在一种情况下,我还使用了jdbcTemplate变量,在另一种情况下使用了countRowsInTable() ,以从指定的表中获取行数。
FundingServiceImplSpringJpaTest:
此类扩展了BaseJpaIntegrationTest基类,以利用超类提供的便捷方法。 它使用simpleJdbcTemplate帮助器变量从FUNDINGDETAILS表中获取行数。 我还对sharedEntityManager属性使用createQuery()方法针对无效查询运行了测试。 清单6显示了FundingServiceImplSpringJpaTest类的源。
清单6. FundingServiceImplSpringJpaTest类
package com.ideature.agiletestingspring.loanapp.service;
import java.util.Collection;
import java.util.Date;
import javax.persistence.EntityManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.internal.runners.TestClassRunner;
import org.junit.runner.RunWith;
import org.springframework.test.annotation.ExpectedException;
import com.ideature.agiletestingspring.loanapp.BaseJpaIntegrationTest;
import com.ideature.agiletestingspring.loanapp.LoanAppConstants;
import com.ideature.agiletestingspring.loanapp.domain.BorrowerDetails;
import com.ideature.agiletestingspring.loanapp.domain.FundingDetails;
import com.ideature.agiletestingspring.loanapp.domain.LoanDetails;
import com.ideature.agiletestingspring.loanapp.repository.BorrowerDetailsRepository;
import com.ideature.agiletestingspring.loanapp.repository.FundingDetailsRepository;
import com.ideature.agiletestingspring.loanapp.repository.LoanDetailsRepository;
import com.ideature.agiletestingspring.loanapp.repository.RepositoryException;
@RunWith(TestClassRunner.class)
public class FundingServiceImplSpringJpaTest extends BaseJpaIntegrationTest {
private static final Log log = LogFactory.getLog(FundingServiceImplSpringDITest.class);
private LoanDetailsRepository loanDetailsRepository = null;
private BorrowerDetailsRepository borrowerDetailsRepository = null;
private FundingDetailsRepository fundingDetailsRepository = null;
public void setLoanDetailsRepository(LoanDetailsRepository loanDetailsRepository) {
this.loanDetailsRepository = loanDetailsRepository;
}
public void setBorrowerDetailsRepository(BorrowerDetailsRepository borrowerDetailsRepository) {
this.borrowerDetailsRepository = borrowerDetailsRepository;
}
public void setFundingDetailsRepository(FundingDetailsRepository fundingDetailsRepository) {
this.fundingDetailsRepository = fundingDetailsRepository;
}
@Before
public void initialize() throws Exception {
super.setUp();
}
@After
public void cleanup() throws Exception {
super.tearDown();
}
@Test
@ExpectedException(IllegalArgumentException.class)
public void testInvalidQuery() {
sharedEntityManager.createQuery("select test FROM TestTable test").executeUpdate();
}
@Test
public void testApplicationManaged() {
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.joinTransaction();
}
@Test
public void testJdbcQueryUsingSimpleJdbcTemplate() {
// Use simpleJdbcTemplate to get the loan count
int rowCount = simpleJdbcTemplate.queryForInt("SELECT COUNT(*) from LoanDetails");
assertEquals(rowCount,0);
}
@Test
public void testInsertLoanDetails() throws RepositoryException {
int loanCount = 0;
Collection
loans = loanDetailsRepository.getLoans();
loanCount = loans.size();
assertTrue(loanCount==0);
long loanId = 200;
LoanDetails loanDetails = loanDetailsRepository.loadLoanDetails(loanId);
assertNull(loanDetails);
double loanAmount = 600000.0;
String loanStatus = LoanAppConstants.STATUS_IN_REVIEW;
String productGroup = "ARM";
long productId = 2345;
double purchasePrice = 700000.0;
// Add a new record
LoanDetails newLoan = new LoanDetails();
newLoan.setLoanId(loanId);
newLoan.setLoanAmount(loanAmount);
newLoan.setLoanStatus(loanStatus);
newLoan.setProductGroup(productGroup);
newLoan.setProductId(productId);
newLoan.setPurchasePrice(purchasePrice);
loanDetailsRepository.insertLoanDetails(newLoan);
loans = loanDetailsRepository.getLoans();
assertEquals(loanCount + 1, loans.size());
}
@Test
public void testLoanFunding() throws RepositoryException {
long loanId = 100;
// -------------------------------------------
// Insert LOAN details
// -------------------------------------------
Collection
loans = loanDetailsRepository.getLoans();
log.debug("loans: " + loans.size());
// Add a new record
LoanDetails newLoan = new LoanDetails();
newLoan.setLoanId(loanId);
newLoan.setLoanAmount(450000);
newLoan.setLoanStatus("REQUESTED");
newLoan.setProductGroup("FIXED");
newLoan.setProductId(1234);
newLoan.setPurchasePrice(500000);
loanDetailsRepository.insertLoanDetails(newLoan);
loans = loanDetailsRepository.getLoans();
log.debug("After adding a new record - loans 2: " + loans.size());
// -------------------------------------------
// Insert BORROWER details
// -------------------------------------------
long borrowerId = 131;
Collection
borrowers = borrowerDetailsRepository.getBorrowers();
log.debug("borrowers: " + borrowers.size());
// Add a new Borrower
BorrowerDetails newBorr = new BorrowerDetails();
newBorr.setBorrowerId(borrowerId);
newBorr.setFirstName("BOB");
newBorr.setLastName("SMITH");
newBorr.setPhoneNumber("123-456-7890");
newBorr.setEmailAddress("test.borr@abc.com");
newBorr.setLoanId(loanId);
borrowerDetailsRepository.insertBorrower(newBorr);
borrowers = borrowerDetailsRepository.getBorrowers();
log.debug("After adding a new record - borrowers2: " + borrowers.size());
// -------------------------------------------
// Insert FUNDING details
// -------------------------------------------
long fundingTxnId = 300;
Collection
fundingDetailsList = fundingDetailsRepository.getFundingDetails();
log.debug("FundingDetails: " + fundingDetailsList.size());
// Add a new record
FundingDetails newFundingDetails = new FundingDetails();
newFundingDetails.setFundingTxnId(fundingTxnId);
newFundingDetails.setLoanId(loanId);
newFundingDetails.setFirstPaymentDate(new Date());
newFundingDetails.setFundType(LoanAppConstants.FUND_TYPE_WIRE);
newFundingDetails.setLoanAmount(450000);
newFundingDetails.setMonthlyPayment(2500);
newFundingDetails.setTermInMonths(360);
fundingDetailsRepository.insertFundingDetails(newFundingDetails);
fundingDetailsList = fundingDetailsRepository.getFundingDetails();
log.debug("After adding a new record - # of records: " + fundingDetailsList.size());
// Delete the borrower details record
borrowerDetailsRepository.deleteBorrower(borrowerId);
// Delete the funding details record
fundingDetailsRepository.deleteFundingDetails(fundingTxnId);
// Delete loan details record last
loanDetailsRepository.deleteLoanDetails(loanId); }
}
事务在testInsertLoanDetails()方法的末尾回滚 。 这就是为什么即使我们调用insertLoanDetails方法来插入借贷记录,但回滚事务时仍会撤消数据库插入。 这样,我们不必担心在执行集成测试后在表中保留任何测试数据,也不必运行任何特殊的数据库清理脚本来删除在测试期间创建的测试数据。
为了测试事务状态, testLoadLoanDetails()方法调用事务管理器帮助器方法endTransaction()和startNewTransaction()来提交当前事务并分别获取新事务。 新事务将启动新的工作单元(UOW),其中系统中不存在LoanDetails域对象。 这些辅助方法对于测试ORM工具(例如Hibernate , TopLink和OpenJPA)中的延迟加载功能很有用。 注意:这些辅助方法也可以在Java 5之前的应用程序中使用。
AllIntegationTests:
最后,还有一个AllIntegationTests测试套件,可以一次执行所有与Funding Service相关的测试。 清单7显示了此测试套件类的源代码。
清单7. FundingServiceImplSpringJpaTest类
package com.ideature.agiletestingspring.loanapp;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
import com.ideature.agiletestingspring.loanapp.service.FundingServiceImplIntegrationTest;
import com.ideature.agiletestingspring.loanapp.service.FundingServiceImplSpringDITest;
import com.ideature.agiletestingspring.loanapp.service.FundingServiceImplSpringJpaTest;
import com.ideature.agiletestingspring.loanapp.service.FundingServiceImplTest;
@RunWith(Suite.class)
@SuiteClasses(value = {
FundingServiceImplTest.class,
FundingServiceImplIntegrationTest.class,
FundingServiceImplSpringDITest.class,
FundingServiceImplSpringJpaTest.class
})
public class AllIntegrationTests {
}
要执行这些测试,请确保在测试类路径中指定了配置文件( loanapp-applicationContext-jpa.xml )位置。 您可以启用Log4J日志记录以验证应用程序上下文正在加载Spring Bean。 查找CachedIntrospectionResults DEBUG消息,这些消息显示了实体管理器,数据源,事务管理器和运行集成测试所需的其他对象的加载。
结论
通过为Java EE开发人员提供一个简单而强大的框架来在容器外部编写和运行单元测试和集成测试,Spring使测试驱动的J2EE应用程序开发成为现实。 它的非侵入式配置管理,使用模拟对象的依赖注入以及对难以存根的API的一致抽象,使容器外的单元测试变得容易。 它的测试模块使用依赖注入(DI)和面向方面的编程(AOP)技术来创建基础,我们可以在此基础上构建单元测试和集成测试。
使用Spring测试类编写测试的一些最佳实践如下:
- 确保在集成测试中与在已部署环境中具有相同的Spring配置文件,因此您不必担心在将应用程序部署到生产环境中时可能引起问题的任何差异。
- 使用Spring框架时,需要牢记一些与数据库连接池和事务基础结构有关的差异。 如果要部署到功能强大的应用程序服务器,则可能会使用其连接池(可通过JNDI获得)和JTA实现。 因此,在生产中,您将使用JndiObjectFactoryBean作为DataSource和JtaTransactionManager 。 JNDI和JTA在容器外集成测试中将不可用,因此对于这些测试,我们应该使用Commons DBCP BasicDataSource和DataSourceTransactionManager或HibernateTransactionManager 。
- Spring的集成测试支持不能替代真正的回归测试。 应用程序功能的回归测试与最终用户在生产环境中实施该应用程序时将如何使用它进行了尽可能接近的测试。
从项目一开始就应该考虑和计划测试,并让质量检查团队参与进来。 我们应该编写单元测试以涵盖主类中多达场景和路径(包括异常路径)的内容。 测试驱动开发(TDD)是通过我们在项目中编写的代码来实现所需测试覆盖率和生产质量的好方法。 如果我们无法在项目中使用该TDD,则应至少尝试另一个TDD(开发过程中的测试),并确保在将代码部署到集成环境(通常是测试环境)之前已对代码进行了单元测试。
资源资源
- Spring测试文档
- 《行动中的春天》,第二版 ,与雷恩·布雷登巴赫(Ryan Breidenbach)的克雷格·沃尔(Craig Walls),曼宁出版社
- 使用Spring进行系统集成测试 ,Rod Johnson,2006年Spring体验。
- Java持久性API
- Spring JPA文档
spring框架用什么软件