This article is based on Spring in Practice, to be published August-2011. It is being reproduced here by permission from Manning Publications. Manning publishes MEAP (Manning Early Access Program,) eBooks and pBooks. MEAPs are sold exclusively through Manning.com. All pBook purchases include free PDF, mobi and epub. When mobile formats become available all customers will be contacted and upgraded. Visit Manning.com for more information. [ Use promotional code ‘java40beat’ and get 40% discount on eBooks and pBooks ]
also read:
Happy Path Testing
Introduction
Among the simplest sort of integration tests are those that test for routine, nonexceptional behavior, often called “happy path” behavior in testing circles. This might involve for instance requesting an object from a web-based interface, having all the backend transactional magic happen (for example, hitting a database), and then verifying that the returned result is what’s expected. As this type of test forms the basis for more sophisticated tests, it makes for a good starting point, so we’ll explore happy path integration testing in this recipe.
Key technologies
Spring TestContext Framework, JUnit, DBMS
Problem
Write “happy path” integration tests to verify correct integration between web, service, DAO and database components.
Solution
As a general statement, integration testing involves selecting large vertical slices of an application’s architecture and testing such slices as collaborating, integrated wholes. Ideally we’re able to reuse as much of the app’s native configuration as possible, partly to minimize code duplication, but more fundamentally to put the configuration itself to test. It is after all part of what makes the app work. In the normal situation we fall short of that ideal because we don’t want to run our integration tests against live production databases. But we can get pretty close. If we can identify the relevant slices and make it easy to choose between the test and production databases, we have a winner.
We’ll begin then with details on how to implement the strategy just outlined using Spring. Spring’s approach to configuration makes it easy and elegant to do this.
Structuring app configuration to facilitate integration testing
In figure 1 we highlight the part of the stack we’re going to address with our approach to integration testing. While we don’t quite hit 100% of the stack (we’re excluding the DispatcherServlet and JSPs from our scope), what we do get represents a reasonable balance between coverage on the one hand, and execution speed and ease of implementation on the other.
Figure 1 We will write integration tests for the stack that starts from the controller and goes all the way back to the database
As illustrated, our stack starts from the controller and pushes all the way back to the database. It bears repeating that we can certainly write integration tests that are more aggressive about coverage—tests that include the DispatcherServlet and JSPs, for example. And in the case of the DispatcherServlet, there are strong reasons for doing so, among them the desire to verify that controller annotations (for example, @InitBinder, @RequestMapping, @PathVariable, @Valid, and so on)1 do what they’re supposed to do. But we’d take a hit for expanding that coverage, either by making the testing more complicated (we’d have to provide the DispatcherServlet configuration, which is more involved than controller configuration) or else by slowing down the execution (for instance, by running the tests in-container using a look like Cactus).
Now that we’ve identified our stack, we need to figure out how to implement the wiring one time, and then reuse that wiring across both normal app use and integration test contexts. That’s actually easy to do. In essence two things differ between the app’s normal configuration and its integration testing configuration:
- The database itself
- How we get the DataSource (JNDI lookups are available in JavaEE container environments, but require more work to establish outside such environments)
So what we want to do is simply carve off the DataSource definition from the rest of the configuration and select a definition based on the context.
We can do this by having two separate DataSource bean configuration files. For normal app execution, the app would use the bean configuration shown in listing 1.
Listing 1 beans-datasource.xml, for normal app usage
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd"> <jee:jndi-lookup id="dataSource" jndi-name="jdbc/sip10DS" resource-ref="true" /> </beans>
That’s just the DataSource lookup; we configure the DataSource itself via whatever container-specific means the container makes available. The sample code for example uses /WEB-INF/jetty-env.xml to configure the DataSource.
The application pulls this configuration into the fold using the normal contextConfigLocation means available through web.xml. We’ve seen that configuration sufficiently many times by now that we won’t repeat it here, but look at web.xml in the sample code if you’d like to see it again.
For integration tests, our bean configuration is considerably different. We don’t have a JNDI environment available, so we need to both build and configure a DataSource, as shown in listing 2.
Listing 2 beans-datasource-it.xml, for integration testing
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/ [CA]spring-context-3.0.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd"> <context:property-placeholder location="classpath:sip10-it.properties" /> #1 <bean id="dataSource" #2 class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" p:driverClassName="${dataSource.driverClassName}" p:url="${dataSource.url}" p:username="${dataSource.username}" p:password="${dataSource.password}" /> <jdbc:initialize-database data-source="dataSource" #3 ignore-failures="DROPS"> <jdbc:script location="classpath:mysql/sip10-schema-mysql.sql" /> <jdbc:script location="classpath:mysql/sip10-test-data-mysql.sql" /> </jdbc:initialize-database> </beans> #1 Externalize DataSource config #2 Create a DataSource #3 Set database to known state
At [#1] we observe our standard practice of externalizing volatile configuration like usernames/password information. Next we build a BasicDataSource [#2] using that configuration, instead of performing a JNDI lookup, since we aren’t in a JavaEE container environment.
Finally, at [#3] we see something new for which there’s no counterpart in listing 1. At a high level, this part of the configuration resets the test database to a known state, which we obviously desire in order to have predictable and repeatable testing. The configuration (the jdbc namespace appeared in Spring 3) causes Spring to run the referenced SQL scripts in the given order against the referenced DataSource whenever we load this application context, typically just before running our integration test suite. The optional ignore-failures=”DROPS” attribute just says that if a script attempts to drop a table and fails (perhaps because the table doesn’t yet exist), continue running the script anyway.
We haven’t yet seen the integration testing counterpart to web.xml for specifying the configuration files we want, but we’ll see that when we get to writing the integration test case itself (listing 5). Before we do that, however, let’s look quickly at the SQL scripts we’re using to reset the test database prior to running the integration tests.
SQL scripts for integration testing
Listing 3 contains the database DDL—just a single table—for our test database, based on MySQL’s SQL dialect. The DDL file is located at src/it/resources/mysql/sip10-schema-mysql.sql so that can locate it conveniently.
Listing 3 sip10-schema-mysql.sql, the integration test DDL
drop table if exists contact; create table contact ( id bigint unsigned not null auto_increment primary key, last_name varchar(40) not null, first_name varchar(40) not null, mi char(1), email varchar(80), date_created timestamp default 0, date_modified timestamp default current_timestamp on update current_timestamp, unique index contact_idx1 (last_name, first_name, mi) ) engine = InnoDB;
Listing 4 contains some simple test data that we’ll use to populate the contact table. This time our SQL script is located at src/it/resources/mysql/sip10-test-data-mysql.sql.
Listing 4 sip10-test-data-mysql.sql, the test data
insert into contact values (1, 'Zimmerman', 'Robert', 'A', '[email protected]', null, null); insert into contact values (2, 'Osbourne', 'John', 'M', '[email protected]', null, null); insert into contact values (3, 'Mapother', 'Tom', 'C', '[email protected]', null, null); insert into contact values (4, 'Norris', 'Carlos', 'R', '[email protected]', null, null); insert into contact values (5, 'Johnson', 'Caryn', 'E', '[email protected]', null, null); insert into contact values (6, 'Smith', 'John', null, '[email protected]', null, null); insert into contact values (7, 'Smith', 'Jane', 'X', null, null, null);
There isn’t too much to say about these scripts. The DDL script drops the table and recreates it, which should provide a solid reset. The DML script feeds the table with test data that we can use for our integration testing.
In the following subsections we’re going write three separate happy path integration tests, demonstrating different key capabilities of the Spring TestContext Framework. The first is the very simplest test, which involves asking for a specific contact and verifying that we got the information we expected to get.
Happy path integration test #1: getting a contact
The bare-bones sample contact management app is just a master list of contacts with editable details pages corresponding to individual contacts. Each details page comes prepopulated with the contact’s data. Figure 2 shows what a details page looks like.
Figure 2 A simple contact details page that comes prepopulated with subterranean, homesick contact data
When I said bare-bones, I wasn’t kidding. There’s just a name and an e-mail address. Our first integration test will test this details page feature. Listing 5 shows how to do this.
Listing 5 ContactControllerIT.java, the integration test case
package com.springinpractice.ch10.web; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import javax.inject.Inject; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Value; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import com.springinpractice.ch10.model.Contact; @RunWith(SpringJUnit4ClassRunner.class) #1 @ContextConfiguration({ #2 "/beans-datasource-it.xml", "/beans-dao.xml", "/beans-service.xml", "/beans-web.xml" }) @Transactional #3 public class ContactControllerIT { #4 @Inject private ContactController controller; #5 @Value("#{viewNames.contactForm}") private String expectedContactFormViewName; private MockHttpServletRequest request; private Model model; @Before public void setUp() throws Exception { this.request = new MockHttpServletRequest(); #7 this.model = new ExtendedModelMap(); #8 } @After public void tearDown() throws Exception { this.request = null; this.model = null; } @Test public void testGetContactHappyPath() { String viewName = controller.getContact(request, 1L, model); #9 assertEquals(expectedContactFormViewName, viewName); #10 Contact contact = (Contact) model.asMap().get("contact"); #11 assertNotNull(contact); assertEquals((Long) 1L, contact.getId()); assertEquals("Robert", contact.getFirstName()); assertEquals("A", contact.getMiddleInitial()); assertEquals("Zimmerman", contact.getLastName()); assertEquals("[email protected]", contact.getEmail()); } } #1 Use TestContext Framework #2 Configure test context #3 Run tests in transactions #4 POJO test case #5 Top of stack #6 EL works here #7 Using Spring mock object #8 ExtendedModelMap works fine #9 Exercise code #10 Verify view name #11 Verify contact info
There is quite a bit happening in listing 5, so we’re going to break it down into digestible pieces.
The very first thing to notice is that our test case class is an annotated POJO [#4]. It does not extend one of the now-deprecated (as of Spring 3) JUnit 3.8 base classes (for example, AbstractDependencyInjectionSpringContextTests). The annotated POJO approach is based on Spring’s TestContext Framework, and as of Spring 3 it is probably fair to describe it as the standard approach to Spring-based integration testing, even though there are in fact optional, non-deprecated base classes for JUnit 4.5+.
The @RunWith(SpringJUnit4ClassRunner.class) annotation [#1] is a JUnit annotation pointing at a Spring class. This tells the test execution environment (Failsafe, for instance) to run tests using SpringJUnit4ClassRunner. At the highest level, this means to run the test case using the Spring TestContext Framework. At a slightly more detailed level, it means:
- Load the test context (that is, bean definitions) from locations we’ll specify, reusing most of the application context configuration files.
- Inject test fixture instances, and resolve any Spring EL expressions.
- Cache the test context between test executions unless otherwise directed.
- Wrap tests in transactions as directed, typically rolling back the transaction when the test completes.
- Honor not only standard JUnit test annotations like @Test and @Ignore, but also Spring TestContext Framework annotations like @ExpectedException, @Timed and @Repeat. (We’ll see these and other framework annotations in upcoming recipes.)
At [#2] we use the @ContextConfiguration annotation to tell the test execution environment where to find the bean configuration files. Notice that we’re getting our DataSource from beans-datasource-it.xml instead of beans-datasource.xml, thanks to the work we did earlier in separating the DataSource configuration out. This is the test case counterpart to the contextConfigLocation parameter from web.xml.
The rules for specifying the @ContextConfiguration locations are explained by the Javadoc for AbstractContextLoader#modifyLocations as follows:
A plain path, for example, “context.xml”, will be treated as a classpath resource from the same package in which the specified class is defined. A path starting with a slash is treated as a fully qualified class path location, for example: “/org/springframework/whatever/foo.xml”. A path which references a URL (for example, a path prefixed with classpath:, file:, http:, and so on) will be added to the results unchanged.
At [#3], the @Transaction annotation indicates that we want to wrap each test with a transaction. That way we can roll back any database mischief we create at the end of each test. We’ll just reuse the transactional machinery from the application’s bean configuration files. (You might be seeing at this point why the TestContext Framework is nice to have around.) If we wanted to specify a custom PlatformTransactionManager bean name (the default is transactionManager) or turn off the default rollback-on-test-completion behavior, we could add a @TransactionConfiguration annotation here, but we don’t want to do either of those things. We’re happy.
We already covered [#4], so let’s start looking inside the class itself. At [#5] we have an autowired controller-class-under-test (using the standardized JavaEE @Inject annotation that Spring 3 supports), and the framework will in fact perform that dependency injection on our behalf. Really it’s not just the controller under test, but rather the whole stack under the controller. The controller however gives us something to poke and prod, and we can watch it to see what happens.
The framework will resolve the EL at [#6] to give us an expected view name so we can stay DRY. Our test setup method just creates a request and a model. The request object [#7] is a mock object, and is part of Spring’s wider offering around mock web objects for testing.3 On the other hand, at [#8] we’ll just create a real ExtendedModelMap just because there’s no reason to prefer a mock to the real thing here.
With all that test case setup (whew!), we’re finally ready to write our first happy path test. We exercise the code by asking the controller to get the contact with ID 1, put it on the model and return a view name [#9]. Then at [#10] we verify that the expected view name matches the actual view name. At [#11] and below we verify that the returned contact matches the SQL test data we saw in listing 4.
Again, that was a lot to digest, so please be sure to review as necessary before moving on. If you’re ready, we’re going to do a slightly more interesting happy path test, this time updating a contact.
Happy path integration test #2: updating a contact
Updating a contact is what we do when we press the submit button on the form we saw in figure 2, so it’s the logical next test. Listing 6 shows an updated version of ContactControllerIT.java, suppressing code we’ve already seen.
Listing 6 Testing contact updates
package com.springinpractice.ch10.web; import javax.sql.DataSource; import org.hibernate.SessionFactory; import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindingResult; ... other imports as before ... ... @RunWith, @ContextConfiguration, @Transactional ... public class ContactControllerIT { private static final String SELECT_FIRST_NAME_QUERY = "select first_name from contact where id = ?"; @Inject private SessionFactory sessionFactory; @Inject private DataSource dataSource; @Value("#{viewNames.updateContactSuccess}") private String expectedUpdateContactSuccessViewName; private SimpleJdbcTemplate jdbcTemplate; ... other fields as before ... @Before public void setUp() throws Exception { this.jdbcTemplate = new SimpleJdbcTemplate(dataSource); #1 ... other setup as before ... } @After public void tearDown() throws Exception { this.jdbcTemplate = null; ... other teardown as before ... } @Test public void testUpdateContactHappyPath() { Contact contact = new Contact(); #2 contact.setFirstName("Bob"); contact.setLastName("Dylan"); contact.setEmail("[email protected]"); BindingResult result = new BeanPropertyBindingResult(contact, "contact"); #3 String viewName = controller.updateContact(request, 1L, contact, result); #4 assertEquals(expectedUpdateContactSuccessViewName, viewName); #5 Model anotherModel = new ExtendedModelMap(); #6 controller.getContact(request, 1L, anotherModel); Contact updatedContact = (Contact) anotherModel.asMap().get("contact"); assertEquals("Bob", updatedContact.getFirstName()); String firstName = jdbcTemplate. #7 queryForObject(SELECT_FIRST_NAME_QUERY, String.class, 1); assertEquals("Robert", firstName); sessionFactory.getCurrentSession().flush(); #8 String updatedFirstName = jdbcTemplate. #9 queryForObject(SELECT_FIRST_NAME_QUERY, String.class, 1); assertEquals("Bob", updatedFirstName); } } #1 To verify DB state #2 Prepare "updated" contact #3 Create test dummy #4 Exercise code #5 Verify view name #6 Verify first name in Hibernate #7 Show update not flushed #8 Flush update #9 Verify first name in DB
Our test for updating a contact looks a lot different than our test for getting a contact, and it is. One major difference is that updating a contact involves a database update that the test framework will roll back on test completion—no matter whether the test is successful or not—to keep the test database in a known, clean state. But that’s not the part that looks different, because we can’t see that at all. The framework handles that transparently, which is one of its selling points.
The part that looks different is that we have to deal with some minor complexity that Hibernate adds. The basic idea is that when we update the controller, this updates a service bean, which updates a DAO, which then updates a Hibernate session. For optimization purposes, Hibernate doesn’t simply flush every change it receives immediately to the database. It will collect changes and flush them either automatically at appropriate points or manually on demand. Our integration test deals with all of this, as we’ll see. Let’s walk through the steps.
In our setup, we create a new SimpleJdbcTemplate using an injected DataSource [#1]. We’ll use this to verify database changes directly instead of relying on Hibernate’s report, since Hibernate session state is somewhat decoupled from database state.
Then we get to our test itself. We need to simulate an update request coming from a web client. To do this, we create a Contact [#2] with the submitted update (here, the original first name ‘Robert’ is being updated to ‘Bob’), create a dummy BindingResult to keep the controller’s updateContact() method happy [#3] and finally exercise the method under test [#4].
Once we’ve exercised the method, it’s time to verify the result. We verify the view name [#5], and then we want to determine whether we were successful in updating the first name. This is a slightly tricky question as explained above. Checking with the controller [#6], it will appear (on running the test) that the answer is yes. And that’s true. But we can use our SimpleJdbcTemplate to determine whether the change made its way all the way back to the database, and the answer there will be no [#7]. We’re still in the middle of a transaction and Hibernate hasn’t flushed the change to the database yet. So we can flush the change manually [#8] and check the database again [#9]. This time, the change is in the database.
Normally we wouldn’t write the first database check (i.e., the check to verify that the change didn’t make it), though it doesn’t hurt anything. We’d simply flush the session and then check the database. But I wanted to show how Hibernate works, and how you’ll need to flush the session and use the SimpleJdbcTemplate if you want to really ensure that your code did what it was supposed to in the database.
We’ll look at one more happy path integration test. This time let’s delete the contact we’ve been working with.
Happy path integration test #3: deleting a contact
In listing 7, we test the deletion of the good Mr. Zimmerman.
Listing 7 Testing contact deletion
package com.springinpractice.ch10.web; import static org.junit.Assert.fail; import com.springinpractice.web.ResourceNotFoundException; ... other imports ... ... @RunWith, @ContextConfiguration, @Transactional ... public class ContactControllerIT { ... various fields ... @Value("#{viewNames.deleteContactSuccess}") private String expectedDeleteContactSuccessViewName; ... setUp(), tearDown(), tests ... @Test public void testDeleteContactHappyPath() { controller.getContact(request, 1L, model); #1 Contact contact = (Contact) model.asMap().get("contact"); assertNotNull(contact); String viewName = controller.deleteContact(1L); #2 assertEquals(expectedDeleteContactSuccessViewName, viewName); #3 try { controller.getContact(request, 1L, new ExtendedModelMap()); #4 fail("Expected ResourceNotFoundException"); } catch (ResourceNotFoundException e) { /* OK */ } String firstName = jdbcTemplate. #5 queryForObject(SELECT_FIRST_NAME_QUERY, String.class, 1); assertEquals("Robert", firstName); sessionFactory.getCurrentSession().flush(); try { jdbcTemplate.queryForObject( SELECT_FIRST_NAME_QUERY, String.class, 1); fail("Expected DataAccessException"); } catch (DataAccessException e) { /* OK */ } } } #1 Verify existence #2 Exercise code #3 Verify view name #4 Verify deletion #5 Flush to DB and verify
By now we’ve seen this a couple of times so we’ll blast through this. We start with a quick check to make sure the contact we’re about to delete (it’s Bob Dylan again) actually exists [#1]. Then we exercise the code [#2] and verify the view name [#3]. Then we try to get the contact from the controller [#4]. We expect a ResourceNotFoundException; we fail the test if we don’t get one. Finally, we run through the same JDBC routine where we flush the session and then verify that the contact is removed from the database. Once again, we didn’t have to check the database twice; we did that just to demonstrate that the flush is required.
That’s happy path integration testing in Spring.
also read:
Summary
This recipe covered a lot of ground. Even though we focused on the basic happy path integration test, we learned how the Spring TestContext Framework supports test database resets, configuration reuse, dependency injection of test fixtures, transactions and rollbacks, mocks, assertions against database state and more. Fortunately, “basic training” was also the most grueling we’ll see. In the following recipes we’ll be able to build in a more leisurely fashion upon the knowledge we’ve just gained.