Introduction
Testing is an integral part of enterprise software development. It is as important, if not the most important, as any other phase of the Software Development Lifecycle (SDLC). But testing an enterprise application is easier said than done. There are several constraints that make testing effort in a project a major challenge. These constraints usually fall into two categories: framework related constraints and project methodology related constraints.
An example of framework related constraints is that J2EE architecture model does not take into account the aspect of unit testing as part of the software development. Since the container (application server) is the core component of J2EE run-time architecture, it is difficult to test applications built based on J2EE API, outside the container. Unit testing (outside the container) is essential to achieve high test coverage. It's also easy to reproduce many failure scenarios without the overhead of application server setup and code deployment. It is vital to ensure that tests can be run quickly which is important in development or production support phases of the project. The task of verifying code using unit testing minimizes unproductive time spent waiting for the application to be deployed every time we make a code change.
Since the code in a traditional J2EE application depends heavily on the application server, its functionality can be fully tested only when deployed inside a J2EE container. But in-container testing is too slow and too much of an obstacle to developer productivity especially if the project is relatively large and number of code artifacts (java source files) is high.
There are some J2EE frameworks that are built from ground-up to provide excellent support for integrating software testing into the development process. Spring is one of such java enterprise application development frameworks.
Recently, I did some consulting on an enterprise java application my wife worked on for a local Tier-One automotive supplier company. The project was to create a Customer Profile Management System used to track the profiles of their customer companies. The architecture of this application included Hibernate 3.0, Spring 2.0, and JBoss 4.0 technologies. The project team followed an Agile software development approach to deliver the requirements in one-week iterations. They used the integration testing features offered by Spring framework, to test the application code in Data Access and Service layers. We really liked the testing support provided by Spring framework. It simplified testing to a great extent and made the aggressive one-week development iterations possible and manageable.
This article provides an overview of the support provided by Spring framework in the areas of unit and integration testing. I will use a sample loan processing web application to help the readers in implementing an Agile Testing framework in a typical Java EE application and how to use Spring test classes to test the application functionality.
Agile Software Testing
A software development project must include good design and architecture practices as well as good testing practices. An application may have a very good architecture, design and code, but if it's not well tested, it cannot be considered a successful product. Some companies (software service vendors) live and die by the quality of their products and testing is crucial in the success of these companies.
Agile software development requires a comprehensive testing strategy in order to achieve the agility and quality in the software development project. Agile testing includes unit testing as well as integration testing. This means that we should be able to execute the tests as quickly as possible (one way to achieve the agility is to run the tests outside the application server). Test Driven Development (TDD) is one of the key elements of Agile development process. Spring and other light-weight containers such as PicoContainer and HiveMind provide great support for test driven software development.
Let's briefly look at the significance of unit and integration testing in a typical Java EE development project and objectives and constraints of each testing approach.
Unit Testing
Unit testing is used to test a specific unit (class) in the application. Unit tests should be written to test all the methods in the class including all exception paths in the methods. The objective of unit testing is to be able to quickly test any new code or changes to existing code without the overhead and additional time involved in tasks such as server configuration, services setup and application deployment. Developer unit testing is critical since it is easier and cheaper to find and fix bugs earlier in software development lifecycle (at coding and unit testing phases) rather than in the later stages.
JUnit is the popular testing framework used for writing unit tests. In JUnit tests, we simply instantiate the objects using new
operator, without having to worry about the container's JNDI resources and J2EE services such as resource pooling, JDBC connection pools and JMS queues. We can also use testing techniques like Mock Objects to test the code in isolation. With unit testing, there is no need for any infrastructure setup for an application server or even a database server.
There are some limitations to unit testing. Unit tests don't address the testing of functional requirements of the application. These tests only cover testing of each module in the application. Also, we can't test scenarios like asynchronous services which require JMS message queues configured inside the application server. But we should still be able to unit test as much application functionality as possible and use the in-container tests for only those functions that cannot be tested outside the container.
Integration Testing
Unit tests are very useful to test a module or class in isolation. But it's also important to do integration testing of the application to see how various modules would work together when assembled in the integrated environment. Some functions that work fine at module level may not work correctly when integrated with other modules in the application. This scenario is very realistic in an agile development environment where different developers work on different parts of the application at the same time and they need to merge code changes on a regular (in some development teams daily) basis. The integration tests include testing round-trip calls between client and service layers of the application. Most of the integration tests usually run in the container. But to be truly agile, we will need to run atleast some integration tests without any code deployment to the container.
Integration tests are useful in DAO layer where the implementations of DAO interfaces cannot be unit tested effectively. Other objectives of integration testing is to test aspects such as remote service, state (session) management, web flow and transaction management. Integration testing has some constraints as well. It takes longer time to run these tests. Since the application needs to be deployed inside Java EE container, there is also the server setup and configuration overhead involved in running these tests.
It should be noted that integration testing is complimentary testing, not a substitute for unit testing. Developers should first write sufficient unit tests for each java class in the application to achieve a good code coverage. At the same time, there should be enough integration tests written to cover different use case scenarios in the application that can not be tested with unit tests.
There are several other types of testing in addition to unit and integration tests. Following table lists different testing strategies and their objectives.
Table 1. Java EE Testing Strategies
Test Strategy | Objective |
---|---|
Unit Testing | Test the application at class level to test all methods in each class. |
Mock Objects | Mock Objects are used in Client and Service layers of the application to test the class methods without having to really connect to back-end database or other network resources. |
ORM Testing | Verify the integrity of database table mappings defined in ORM layer. These test classes use database metadata information to check ORM mappings. |
DB Testing | Test data access classes (DAOs) in isolation. These tests put the database tables into a known state for each test run. |
XML Testing | Test XML documents, their validity, and compare two different documents to assert if they are same or not. |
Integration Testing | Test website navigation, web flow, and state (session) management and transaction management. |
Regression Testing | Test the application functionality as an end-user would use the application when it's deployed in production environment. These tests are usually run by a dedicated QA team using automated testing tools such as Mercury QuickTest Professional (QTP). |
Load Testing | Test the scalability of the application. These performance tests are usually run by a dedicated load testing team using tools such as Mercury LoadRunner, WAPT, and JMeter. |
Profiling | Test for any memory leaks, memory usage, garbage collection etc at application run-time. Developers run the application through a java profiler such as JProfiler, Quest JProbe, Eclipse Test and Performance Tools Platform (TPTP). |
There are a variety of open source testing frameworks to perform the various test strategies listed above. Following is a list of some of these frameworks.
- JUnit
- JMock
- ORMUnit
- DBUnit
- XMLUnit
- JUnitEE
- MockEJB
- Cactus
Since the testing aspect determines the success or failure of a project, any Java EE framework we use for software development should provide the support for seamless integration of testing into design and development phases. Let's look at some of the characteristics that an ideal Java EE framework should have, from unit and integration testing stand-point.
Agile Development:
The framework should aid in an Iterative and Agile software development of the application. Agile methodologies are being adapted by more development teams and agile testing and early feedback is the main part of iterative development.
Test Driven Development:
It is a well proven fact that the testing concern needs to be addressed right from the early phases of application development lifecycle. It is lot cheaper and more effective to find and fix the bugs earlier in the process. And the best way to find any bugs is by testing "early and often" in design and development phases of each iteration in the project.
Interface based design:
One of the best practices that we object oriented programmers try to strive for is to write java classes to interfaces instead of concrete classes. Writing to interfaces gives us a great flexibility in running the unit and integration tests without having to modify the client code whenever the implementation of a service component is changed.
Separation of Concerns:
We achieve a "separation of concerns" (SOC) when we consciously design and write code in separate modules based on the specific application concern that we are trying to address (like domain, business logic, data access and infrastructure logic). This way different concerns like logging, exception handling and application security can be tested in isolation without any dependencies on other modules.
Layered Architecture:
A typical Java enterprise application is designed in such a way that there are client, service, domain and persistence layers. Any element of a layer should only depend on other elements in the same layer or on elements of the layer just "beneath" it (assuming that presentation is the topmost layer and persistence is the bottom most layer in the architecture layers). What this means is the client layer can only depend on service layer. Service layer can only depend on Domain layer and domain layer can only depend on persistence layer. The Java EE framework should support unit and integration testing in all these layers without any dependency on other layers.
Non-Invasive:
Frameworks like EJB and Struts force developers to extend framework specific classes (like EJBObject, ActionForm, Action etc) in the application. This creates an application dependency on the specific framework which makes unit testing a challenge and also leads to extra work when we need to switch to a different (and better) framework. These frameworks are invasive in nature and should be carefully chosen with future extensibility requirements in mind. Note that the latest version of EJB specification (version 3.0), which is part of Java EE 5, is less invasive as the Entities (formerly called Entity Beans) and Session beans are plain java classes that can be tested outside the container similar to Spring beans.
Inversion Of Control (IoC):
The framework should provide support for Inversion of Control on the objects created in the application. Inversion Of Control or IoC (also known as Dependency Injection, DI) design pattern brings several benefits to integration testing. The primary benefit is that the application designed based on IoC pattern depends far less on the container than one created using traditional J2EE application architecture.
Aspect Oriented Programming (AOP):
AOP enables behavior, that would otherwise be scattered through different classes, to be centralized in a single module. It's very useful in unit and integration testing where we can test Java EE services such as transaction management and role based security declaratively using Aspects.
Service Oriented Architecture:
Testing is a critical component in a SOA infrastructure since a Service is used in different modules and applications in the enterprise. If a specific use case of a service component is not tested completely, it could potentially cause production problems and quality issues when code changes are implemented in the production environment.
Data Access Abstraction:
A consistent architectural approach to data access is also very important in testing the data access functions of an application. The data access abstraction should be agnostic to any persistence implementation framework (such as Hibernate, JPA, JDO, iBATIS, OJB, and Spring JDBC). It should also nicely handle data access exceptions thrown in the persistence layer.
Transaction management:
The framework should provide an abstraction interface for testing transaction management. It should integrate with JDBC and JTA transactions (for both Container and Bean managed transactions) and other transaction objects such as Hibernate Transaction.
Spring Support For Testing
Spring framework was designed based on agile testing strategies to help developers follow sound design and effective unit testing best practices. It also provides a great support for running integration tests outside the application server. Spring is a non-invasive framework in the sense that when we use Spring, there is a minimal dependency of application code on the framework. We can configure application objects as plain java classes (POJOs) without having to extend any Spring specific classes (Note: When you use Spring template helper classes such as JDBCTemplate, JNDITemplate, HibernateDaoSupport etc, you are adding the dependency on the Spring framework). We can even configure any legacy classes that were written prior to the existence of Spring.
Spring framework in general, and Spring Testing module in particular support the following aspects:
Isolation:
Spring gives J2EE developers the flexibility to test java classes in isolation by injecting mock implementations. For example, we can test a service class using a mock implementation of the corresponding Repository class. This way, we can test business logic in the service class without worrying about the persistence details of connecting to a database.
Inversion of Control:
The framework provides sophisticated configuration management for POJOs. Spring IoC container can manage fine or coarse grained java classes. It uses a bean factory to instantiate application objects and wire them together using constructor or setter injection mechanisms.
Data Access:
It provides a nice persistence architecture for data access and a good hierarchy of data access exceptions. It provides helper classes (such as JdbcTemplate, HibernateTemplate, TopLinkTemplate, JpaTemplate) to work with the leading persistence frameworks.
Transaction Management:
Spring provides a nice abstraction framework for managing transactions (both local and global). This abstraction provides a consistent programming model in a wide range of development environments and is the basis for Spring's declarative and programmatic transaction management.
Integration Testing using Spring
Spring configuration, dependency injection (DI), data access (CRUD), and transaction management are some of the concerns that we can test outside the server environment using Spring Testing framework. The data access tests are executed against a real database so no need to use any mock objects in these tests.
Spring context load time may not be an issue in small to medium size web applications. But for a large size enterprise application, it could take significant time to instantiate the classes in the application. Also, the overhead of running every single test case in every single test fixture leads to slower overall test runs and adversely affects developer productivity. Keeping these concerns in mind, Spring development team wrote some test classes that can be used as integration tests running outside the container. Since these test classes are extensions of JUnit API, we get all the benefits of JUnit right out of the box when we use the Spring test classes. These test classes setup a transaction for each test method and automatically cleanup (rolling back the transaction at end of each method) eliminating the necessity for any database setup and teardown tasks.
Following is a list of items we can verify when we run integration tests in a Spring application:
- Loading of Spring contexts and management of the contexts by caching loaded contexts between execution of each test case. Also, verify correct wiring of application contexts by Spring IoC container.
- Dependency Injection of test fixtures and Spring configuration details (To verify if a specific data access (Repository) class configuration is loaded correctly).
- Convenience variables for data access and CRUD operations (The logic of data access class to test database select and updates).
- Transaction management.
- ORM mapping file configuration (Verify if everything related to a persistence object is mapped correctly and correct lazy-loading semantics are in place).
We can run integration tests just like JUnit tests. They are slower to run compared to unit tests because we are testing the code at integration level rather than just at a class level. But these integration tests are lot faster to execute than the tests created using in-container testing frameworks such as JUnitEE or Cactus which rely on deployment of the application to the container before the tests can be executed.
Spring integration test classes were designed to address various testing concerns so there are different test classes in org.springframework.test package. Following table shows some of the test classes provided in Spring framework for integration testing and in what scenarios they can be used.
Table 2. Spring Test Classes
Test Class Name | Description |
---|---|
AbstractDependencyInjection SpringContextTests | This test class injects test dependencies so we don't need to specifically perform the Spring application context lookups. It also automatically locates the corresponding object in the set of configuration files specified in the getConfigLocations() method. |
AbstractTransactionalDataSource SpringContextTests | This test class is used to test the code that runs inside a transaction. It creates and rolls back a transaction for each test case. We write code with the assumption that there is a transaction present. It provides the fields such as a JdbcTemplate that can be used to verify database state after test operations, or verify the results of queries performed by application code. An ApplicationContext is also inherited, and can be used for explicit lookup if necessary. |
AbstractJpaTests | This test class is used for testing JPA functionality. It provides an EntityManager instance that we can use to call JPA methods. |
AbstractAspectjJpaTests | This class extends from AbstractJpaTests and it is used for load-time weaving (LTW) purposes using AspectJ. We override the method getActualAopXmlLocation() to specify the location of AspectJ's configuration xml file. |
AbstractModelAndViewTests | This is a convenient base class for testing the presentation and controller layers (using Spring MVC) in the application. |
Figure 1 below shows the class hierarchy diagram of Spring framework test classes extending from JUnit TestCase class. Note: These test classes are part of spring-mock.jar file (located in dist folder inside Spring framework installation directory).
Figure 1. Spring test class hierarchy (Click on the screen shot to open a full-size view.)
Following is a list of the factors to consider when deciding on which test class to extend from:
- To use the Spring application context without having to write the code to initialize and manage it.
- To test the data access (using data source)
- To test a method inside a transaction (using transactionmanager)
- JDK Version: If you are using JDK 1.4 so you can't take the advantage of Annotations introduced in JDK 1.5.
The following section gives more details of these test classes.
AbstractSpringContextTests:
This is the base class for all Spring test classes. It provides convenience methods to load Spring application context. We extend this class when we need to test loading of Spring context without having to explicitly manage the dependency injection. It maintains a static cache of contexts by key which helps in significant performance benefit if the application has several Spring beans (especially beans such as LocalSessionFactoryBean for working with an ORM tool such as Hibernate) to load. Hence, in most cases, it makes sense to initialize the application context once and cache it for subsequent lookups.
AbstractSingleSpringContextTests:
This is an abstract test class that exposes a single Spring ApplicationContext. It will cache the application contexts based on a context key which is usually the config locations (String[]) describing the Spring resource descriptors. It encapsulates all the functions related to loading and managing the Spring context.
AbstractDependencyInjectionSpringContextTests:
This is a convenient super class for tests that depend on the Spring application context. It has setAutowireMode() method used to set the autowire mode for test properties set by Dependency Injection. The default is AUTOWIRE_BY_TYPE but it can also be set to AUTOWIRE_BY_NAME or AUTOWIRE_NO.
AbstractTransactionalSpringContextTests:
This class has several convenient methods to make the transaction management task easier when running the integration tests. It provides the transaction variables transactionManager, transactionDefinition, and transactionStatus for managing the transactions in the test methods. It also has a method called endTransaction() to force a commit or rollback of the transaction. And startNewTransaction() method is used to start a new transaction, which is called after calling endTransaction().
AbstractTransactionalDataSourceSpringContextTests:
This is one of the mostly used Spring test classes. It provides useful inherited protected fields, such as a JdbcTemplate that can be used to run CRUD operations on the database. It also executes each test method in its own transaction, which is automatically rolled back by default. This means that even if tests change the database state (with insert, update, or delete), there is no need for a teardown or cleanup script because the database changes are automatically rolled back. The other helper methods in this class are countRowsInTable() which is a handy method to check if a new record has been added or an existing record has been deleted, deleteFromTables is used to delete all rows from the tables, executeSqlScript is used to execute a given SQL script (These DB changes will be rolled back based on the state of the current transaction).
AbstractAnnotationAwareTransactionalTests:
This test class exposes SimpleJdbcTemplate variable. Using this class, we can control transactional behavior using @Transactional annotation. We can also use @NotTransactional to prevent any transaction being created at all (Note these two are Spring specific annotations and they create a dependency on Spring framework. You can use this feature only if you are using JDK 1.5.
AbstractJpaTests:
This is base test class to extend from when we want to test a DAO class written using JPA API. It exposes an EntityManagerFactory and a shared EntityManager for persistence methods. It requires DataSource and JpaTransactionManager to be injected.
Once you decide on which test class to extend, here are the steps required to integrate Spring test classes in your integration tests:
- Extend one of Spring test classes (usually AbstractTransactionalDataSourceSpringContextTests or AbstractJpaTests if you specifically want to test JPA functionality). Note that JPA is Java SE 5.0 only so if you are using pre-JDK1.5 version in your application, you can't extend this class.
- Override getConfigLocations() method to load all the configuration files for data source, transaction manager, and other resources used in the application code. Use @Override annotation to specify that we are overriding this method defined in the super class (AbstractSingleSpringContextTests).
- Write setter methods for all the DAO objects used in the test class (these DAO objects are injected by Spring IoC container using the specified auto-wire option).
- Add the test methods using these DAO objects (which in turn use the data source), transaction manager, and ORM persistence helper methods.
Rod Johnson's presentation on System Testing Using Spring is an excellent resource on the support Spring Test API offers in unit and integration testing.
Sample Application
The sample application used in this article is a home loan processing system. The use case is to process the funding of a mortgage in the system. When a loan application is submitted to a mortgage lending company, it first goes through the underwriting process where the underwriters approve or deny loan request based on customer's income details, credit history and several other factors. If the loan application is approved it goes through the closing and funding processes.
Funding module in the loan processing application automates the process of disbursement of funds. Funding process typically starts with the lender company forwarding the loan package to a title company. The title company then reviews the loan package and schedules a date and time for closing the loan. Borrower and the seller meet with the closing agent at the title company.
During closing, the buyer (borrower) and the seller read and sign the final loan documents. The borrower would pay the down payment and money for any fees associated with closing the loan. Also, closing costs and escrow fees are paid at the time of the closing process. After the closing meeting, the title company sends the signed agreement to the lender for funding. The lender transfers the funds to the title company.
Application Architecture:
I followed the popular layered architecture in the design of the sample application. These layers are Presentation, Application (Controller), Service, Domain and Data Access layers. I named the data access classes (DAO) as Repositories following the naming conventions recommended in Domain Driven Design principles. I wrote the service, domain, and data access layers as plain java classes. I won't cover the Presentation and Application layer classes in this article.
The application architecture diagram of LoanApp web application is shown in Figure 2 below.
Figure 2. LoanApp application architecture diagram (Click on the screen shot to open a full-size view.)
Data Model:
I created the loan application database (called LoanDB) for the loanapp application using HSQL database. For the demo purposes, I kept the data model simple with 3 tables called LOANDETAILS, FUNDINGDETAILS, and BORROWERDETAILS.
Domain Model:
There are three domain objects in the model, namely LoanDetails, BorrowerDetails, and FundingDetails to capture the business entities required in the funding module in the loan processing system.
Note: The model used in the sample application is just for demonstration purposes. A real world application domain model would be more complex than what is described here.
I wrote the data access classes for each of the domain classes. The DAO classes are LoanDetailsRepositoryJpaImpl, BorrowerDetailsRepositoryJpaImpl, and FundingDetailsRepositoryJpaImpl. And there is a service class called FundingServiceImpl that encapsulates the logic to process a funding request. It calls the DAO's in order to approve, deny, or void the funding request for a specified loan.
Persistence:
I used Java Persistence API (JPA) as the persistence model for object-relational mapping (ORM) requirements in the web application. Spring provides support for all the leading persistence frameworks such as Hibernate, JDO, iBATIS, TopLink, and JPA. Spring JPA, which is part of Spring 2.0 release, includes JPA helper classes in org.springframework.orm.jpa package. I used the EntityManager option (instead of JPA Template) for the persistence concern. This approach has no dependency on Spring and still can be managed using Spring application context. We can also take advantage of Annotations to inject EntityManager using @PersistenceContext and @PersistenceUnit tags.
Following table shows the frameworks and technologies used in the sample application.
Table 3. Technologies used in the sample application
Layer | Technology | Version |
---|---|---|
Controller | Spring MVC | 2.0 |
Service | Spring | 2.0 |
Domain | Plain Java Classes | |
Persistence | JPA | |
Database | HSQLDB server | 1.8.0 |
Java | Java SE | 6.0 |
Build | Ant | 1.7 |
IDE | Eclipse | 3.3 |
The other tools I used in the sample application are Metrics and FindBugs for static code analyses, EclEmma for code coverage.
Testing
Here is a quick reiteration of what our objectives are in writing and executing the tests.
- We want to code and run the tests without leaving the IDE (Eclipse).
- There should be no special deployment of the code required
- We should be able to exploit other code analysis tools such as Metrics and FindBugs right from within the IDE so we can find any bugs right away and fix those issues.
In addition to the traditional unit tests (using JUnit) for each of the Repository classes in the application (namely LoanDetailsRepositoryJpaImpl, BorrowerDetailsRepositoryJpaImpl, and FundingDetailsRepositoryJpaImpl), I also wrote integration tests to verify FundingServiceImpl class methods.
Following table shows the list of the main and corresponding test classes.
Table 4: List of test cases in the loan application
Application Layer | Main Class | Test Class(es) |
---|---|---|
Data Access | LoanDetailsRepositoryImpl | LoanDetailsRepositoryJpaImplTest, LoanDetailsRepositoryJpaImplSpringDITest |
Data Access | BorrowerDetailsRepository | BorrowerDetailsRepositoryJpaImplTest |
Data Access | FundingDetailsRepositoryImpl | FundingDetailsRepositoryJpaImplTest |
Service | FundingServiceImpl | FundingServiceImplIntegrationTest, FundingServiceImplSpringDITest, FundingServiceImplSpringJPATest |
To compare Spring's integration testing support, I first wrote the funding service integration test without using Spring test classes (FundingServiceImplTest). Then I wrote two other test classes (FundingServiceImplSpringDITest and FundingServiceImplSpringJpaTest) to test the logic in FundingServiceImpl class, but this time using Spring test classes. I used several helper variables and methods to aid in running the database queries. These are variables jdbcTemplate, simpleJdbcTemplate, sharedEntityManager, and methods countRowsInTable(), deleteFromTables(), and endTransaction().
Let's take a look code examples of these unit and integration test classes to see how many boiler-plate testing tasks are automated by Spring Testing API so the developers can focus on asserting the actual business logic.
First of all, let's look at the Spring configuration XML file. This file has the Spring bean definitions for repository (DAO) classes used in the sample application. Listing 1 shows the code for loanApplicationContext-jpa.xml configuration file.
Listing 1. LoanApp Spring configuration details
<?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>
I wrote two base classes that all test classes in the sample application extend from. These are BaseDataSourceSpringContextIntegrationTest and BaseJpaIntegrationTest.
BaseDataSourceSpringContextIntegrationTest:
This is the base test class for testing the data access and Spring context loading functions. It extends Spring's AbstractTransactionalDataSourceSpringContextTests class. It loads the application context by calling getConfigLocations() method. The source code for this abstract test class is shown in Listing 2.
Listing 2. BaseDataSourceSpringContextIntegrationTest base test class
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:
This is the base class for all the integration tests created to test ORM functions using JPA. It extends Spring's AbstractJpaTests class. Listing 3 below shows the code for BaseJpaIntegrationTest class.
Listing 3. BaseJpaIntegrationTest test class
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; } }
The details of the other test classes in LoanApp application are as follows:
LoanDetailsRepositoryJpaImplTest:
This is the plain vanilla repository unit test class to test CRUD logic in LoanDetailsRepositoryJpaImpl class. It explicitly initializes the Spring application context, retrieves loanDetailsRepository from context, and then calls CRUD methods in the repository class. It also calls the delete method to remove the new record added in LOANDETAILS table. This test class also has setUp() and tearDown() methods to initialize and cleanup the resources used in the test methods.
LoanDetailsRepositoryJpaImplSpringDITest:
This test class is similar to LoanDetailsRepositoryJpaImplTest but it uses Spring test classes to make it very easy to test data access methods in LoanDetailsRepository class. It extends BaseDataSourceSpringContextIntegrationTest. It has a setter method for setLoanDetailsRepository(), so Spring's IoC container will inject the correct implementation of the repository interface at runtime. There is no boiler-plate code such as initializing the application context, or setUp() and tearDown() methods. Also, there is no need to call the delete method since all the DB changes are automatically rolled back at the end of each test method. We use AUTOWIRE_BY_TYPE (default option) to auto-wire LoanDetailsRepository using setLoanDetailsRepository() method.
FundingServiceImplIntegrationTest:
This is the test class for FundingServiceImpl class. It shows how much code we have to write if we were not taking the advantage of Spring test classes. Listing 4 shows the code of this integration test class.
Listing 4. Sample code for 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 { Collectionloans = loanDetailsRepository.getLoans();
log.debug("loans: " + loans.size());
// At this time, there shouldn't be any loan records
assertEquals(0, loans.size());
Collectionborrowers = borrowerDetailsRepository.getBorrowers();
log.debug("borrowers: " + borrowers.size());
// There shouldn't be any borrower records either
assertEquals(0, borrowers.size());
CollectionfundingDetailsList = 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);
}
}
}
As you can see in testLoanFunding() method, we have to explicitly call the delete methods in FundingDetailsRepository class to keep the DB state same as it was before running this test.
FundingServiceImplSpringDITest:
This class extends BaseDataSourceSpringContextIntegrationTest base class. It has the setter methods for Repository objects so these will be injected by Spring DI container when the application context is loaded. The source code for this integration test class is shown in Listing 5.
Listing 5. FundingServiceImplSpringDITest test class
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"}); Collectionloans = 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;
Collectionloans = 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);
}
}
The helper method deleteFromTables() is used in this class to delete the data from FUNDINGDETAILS table. This method is available from the Spring test super class. I also used jdbcTemplate variable in one case and countRowsInTable() in another instance, to get the row count from the specified table.
FundingServiceImplSpringJpaTest:
This class extends BaseJpaIntegrationTest base class to take the advantage of the convenience methods that the super class provides. It uses simpleJdbcTemplate helper variable to get the row count from FUNDINGDETAILS table. I also run a test against an invalid query using createQuery() method on sharedEntityManager attribute. Listing 6 shows the source for FundingServiceImplSpringJpaTest class.
Listing 6. FundingServiceImplSpringJpaTest class
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; Collectionloans = 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
// -------------------------------------------
Collectionloans = 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;
Collectionborrowers = 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;
CollectionfundingDetailsList = 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); }
}
The transaction is rolled back at the end of testInsertLoanDetails() method. This is why even though we call insertLoanDetails method to insert a loan record, the database insert is undone when the transaction is rolled back. This way we don't have to worry about leaving any test data in the tables after the integration tests are executed or run any special DB cleanup scripts to delete the test data created during the tests.
To test transaction state, testLoadLoanDetails() method calls transaction manager helper methods endTransaction() and startNewTransaction() to commit the current transaction and get a new transaction respectively. The new transaction starts a new Unit Of Work (UOW) where the LoanDetails domain object doesn't exist in the system. These helper methods are useful for testing lazy loading feature in ORM tools such as Hibernate, TopLink, and OpenJPA. Note: These helper methods work in pre-Java 5 applications as well.
AllIntegationTests:
Finally, there is AllIntegationTests test suite that runs all Funding Service related tests in one execution. Listing 7 shows the source for this test suite class.
Listing 7. FundingServiceImplSpringJpaTest class
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 { }
To execute these tests make sure the configuration file (loanapp-applicationContext-jpa.xml) location is specified in the test classpath. You can enable Log4J logging to verify that Spring beans are being loaded by the application context. Look for CachedIntrospectionResults DEBUG messages that show the loading of entity manager, datasource, transaction manager and other objects required for running the integration tests.
Conclusions
Spring makes test driven J2EE application development a reality by offering Java EE developers an easy and powerful framework to write and run both unit and integration tests outside the container. Its non-invasive configuration management, dependency injection to use mock objects, and consistent abstraction over hard-to-stub APIs makes unit testing outside the container easy. Its testing module uses Dependency Injection (DI) and Aspect Oriented Programming (AOP) techniques to create a foundation on which we can build our unit and integration tests.
Some of the best practices of writing tests using Spring test classes are as follows:
- Make sure you have the same Spring configuration files in the integration tests as in the deployed environment so you don't need to worry about any differences that may cause problems when the application is deployed in the production environment.
- There are some differences that are related to database connection pooling and transaction infrastructure to keep in mind when using Spring framework. If you are deploying to a full-blown application server, you will probably use its connection pool (available through JNDI) and JTA implementation. Thus in production you will use a JndiObjectFactoryBean for the DataSource, and JtaTransactionManager. JNDI and JTA will not be available in out-of-container integration tests, so we should use Commons DBCP BasicDataSource and DataSourceTransactionManager or HibernateTransactionManager for these tests.
- Spring's integration testing support is not a replacement for the real regression testing . Regression testing the application functionality is as close as we can get to how the end-users will use the application when it's implemented in the production environment.
We should make it a practice to think and plan about testing and involve QA team right from the beginning of a project. We should write unit tests to cover as many as scenarios and paths (including exception paths) in the main class. Test Driven Development (TDD) is a great way to achieve the desired test coverage and production quality in the code we write in the project. If we can't get this TDD going in our projects, we should atleast try the other TDD (Test During Development) and make sure the code is unit tested before it's deployed to the integration environment (usually Test environment).
Resources
- Spring Testing Documentation
- Spring in Action, Second Edition, Craig Walls with Ryan Breidenbach, Manning Publications
- System Integration Testing Using Spring , Rod Johnson, The Spring Experience 2006.
- Java Persistence API
- Spring JPA Documentation