Happy Path Testing in Spring TestContext Framework

April 14, 2011

Spring Framework

«»

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 ]

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

1
2
3
4
5
6
7
8
9
10
11
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?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 <jdbc:initialize-database> 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 <jdbc:initialize-database> can locate it conveniently.

Listing 3 sip10-schema-mysql.sql, the integration test DDL

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
insert into contact values (1, 'Zimmerman', 'Robert', 'A',
'bobdylan@example.com', null, null);
insert into contact values (2, 'Osbourne', 'John', 'M',
'ozzyosbourne@example.com', null, null);
insert into contact values (3, 'Mapother', 'Tom', 'C',
'tomcruise@example.com', null, null);
insert into contact values (4, 'Norris', 'Carlos', 'R',
'chucknorris@example.com', null, null);
insert into contact values (5, 'Johnson', 'Caryn', 'E',
'whoppigoldberg@example.com', null, null);
insert into contact values (6, 'Smith', 'John', null,
'johnsmith@example.com', 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.

email

«»

Comments

comments