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
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | 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("bobdylan@example.com", 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
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | 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("bobdylan@example.com");
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
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 35 36 37 38 39 | 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.
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.






April 14, 2011
Spring Framework