An Example of Test Driven Development with the Spring Framework
By Jeff Grigg, OCI Software Engineer
December 2009
Introduction
In this article we will build the simplest possible "enterprise" application with the Spring Framework, using the Test Driven Development practice. Our purpose is to demonstrate best practices and patterns in software development, showing each step from the first unit test to the final operation of the program and integration testing.
These are the main practices and patterns we will be demonstrating:
- The Spring Framework for Dependency Injection in a multi-layer production program
- Test Driven Development - a technique for writing software and ensuring that it is well tested
- The EasyMock library - to generate mock objects
- The use of Spring Framework and EasyMock libraries together, for integration testing.
- The JUnit library - for Automated Regression Testing
- The value of refactoring both test and production code.
- The use of both unit and integration tests to ensure that a program runs as expected.
The Application
To serve as an example, we will build a trivially simple Swing application that does a little filtering. It will have a drop-down menu with three values and a search button:
We will organize the code according to the Model-View-Presenter pattern, to achieve proper separation of concerns into layers, as shown in this Class Diagram of the production classes:
To avoid direct dependencies between classes, we are using Interface Based Programming style, where implementation classes implement and depend upon interfaces, but never directly reference each other.
To explain the purpose of each of the classes and interfaces above...
- The
Model
class will contain the main data processing logic for the application. In this application it is aware of the "database" through theIDatabase
interface, as a source ofString
records to filter. It implements theIModel
interface, to provide theperformSearch
functionality, which filters the data. - The
View
class contains and manages the Java Swing user interface components, implementing theIView
interface, to provide access to these components from within the application code. - The
Presenter
class ties together theModel
andView
components, exchanging data between them to enable user interaction with the application. Having this class provides us a place to put user interface related code that is not intimately tied to theView
implementation logic, so that we can test it independently of the view, and so that we can keep user interface logic out of the application's Model. - The
Database
class provides very simplistic access to an array ofString
data values, used for illustration in this application. In production applications this would typically be a Data Access Object providing access to a relational database or other external storage or services. - And there will be a
MainJavaApplication
class, with the Javamain
method, used to run this Swing application. It will use the Spring Framework to wire together the other components and run the application.
Developing the Presenter Class
We will develop the code using the classic Test Driven Development technique to ensure that our code works as expected. And we will use Spring Dependency Injection to wire the layers together.
First Step Towards Having a Presenter Class
We will start with the first test: To get the drop-down list on the screen to display the values "A only", "B only" and "- ALL -", the Presenter
must return these values when asked. So we write this test:
- package client;
-
- import static org.easymock.EasyMock.createStrictMock;
- import static org.easymock.EasyMock.replay;
- import static org.easymock.EasyMock.verify;
-
- import java.util.Arrays;
-
- import junit.framework.TestCase;
-
- import common.IModel;
- import common.IView;
-
- public class PresenterTest extends TestCase {
-
- public void testUserVisibleSearchSelectionOptions() {
- IModel model = createStrictMock(IModel.class);
- IView view = createStrictMock(IView.class);
- Presenter presenter = new Presenter(view model);
- replay(model, view);
-
- String[] searchOptionSelectionStrings = presenter
- .getSearchOptionSelectionStrings();
-
- assertEquals(Arrays.asList("A only", "B only", "- ALL -"), //
- Arrays.asList(searchOptionSelectionStrings));
- verify(model, view);
- }
- }
Using the EasyMock Library to create simulations of the Model and View objects, we create a Presenter
object, ask it for the correct "search option selection" values, and assert that it returns the expected values: "A only", "B only", and "- ALL -"
At first this code will not compile, for a number of reasons, so we...
- Add the required libraries. (See below for links to these libraries.)
- Create the
client
andcommon
packages in the appropriate source directory. - Create the interface classes.
- Create the
Presenter
class. - Add the minimal amount of code to the
Presenter
class needed to make the test pass.
This gives us the following two interface classes:
- package common;
-
- public interface IModel {
- }
- package common;
-
- public interface IView {
- }
...and this Presenter
class:
- package client;
-
- import common.IModel;
- import common.IView;
-
- public class Presenter {
-
- private static final String[] SEARCH_OPTIONS = new String[] { "A only",
- "B only", "- ALL -" };
-
- public Presenter(IView view, IModel search) {
- }
-
- public String[] getSearchOptionSelectionStrings() {
- return SEARCH_OPTIONS;
- }
- }
That is a good first step: We know the values we want to see in the drop-down list box. And we have tested to ensure that we will get exactly those values. So we can have confidence that it is right.
Second Step Towards Having a Presenter Class
A good second step would be to select one of the values, and to ensure that the Presenter
asks the Model
for the correctly filtered data, and that the Presenter
passes this filtered data to the View
.
Here is the test:
- public void testAOnly() {
- IModel model = createStrictMock(IModel.class);
- IView view = createStrictMock(IView.class);
- Presenter presenter = new Presenter(view, model);
- view.initalizeAndDisplayView(presenter);
- List<String> searchOptions = Arrays.asList(presenter
- .getSearchOptionSelectionStrings());
- expect(view.getSelectedSearchIndex()).andReturn(
- searchOptions.indexOf("A only"));
- expect(model.performSearch("A")).andReturn(
- Arrays.asList("some A value", "another A value"));
- view.setDisplayText("[some A value, another A value]");
- replay(model, view);
- presenter.displayViewOnScreen();
-
- presenter.searchButtonPressed();
-
- verify(model, view);
- }
This pushes us to implement most of the code that the Presenter
class needs, except for a lookup of the selected value, to determine what to send to the Model
:
- package client;
-
- import java.util.List;
-
- import common.IModel;
- import common.IView;
-
- public class Presenter {
-
- private static final String[] SEARCH_OPTIONS = new String[] { "A only",
- "B only", "- ALL -" };
-
- private final IView view;
- private final IModel search;
-
- public Presenter(IView view, IModel search) {
- this.view = view;
- this.search = search;
- }
-
- public void displayViewOnScreen() {
- view.initalizeAndDisplayView(this);
- }
-
- public String[] getSearchOptionSelectionStrings() {
- return SEARCH_OPTIONS;
- }
-
- public void searchButtonPressed() {
- int selectedSearchIndex = view.getSelectedSearchIndex();
- List<String> searchResult = search.performSearch("A");
- view.setDisplayText(searchResult.toString());
- }
- }
We interact with the View
and Model
objects to produce the desired result, but we currently ignore the currently selected value in the view's drop-down and send a hard-coded "A" value to the model. This is intentional; we do not want to write code that is inadequately tested. And we do not yet have tests for searching for anything other than an "A" value. We will write more tests in a moment, but first we will ensure that the existing code and tests are as readable and maintainable as possible.
Refactoring a Test
I find this test code a bit harder to read than I would like:
- public void testAOnly() {
- IModel model = createStrictMock(IModel.class);
- IView view = createStrictMock(IView.class);
- Presenter presenter = new Presenter(view, model);
- view.initalizeAndDisplayView(presenter);
- List<String> searchOptions = Arrays.asList(presenter
- .getSearchOptionSelectionStrings());
- expect(view.getSelectedSearchIndex()).andReturn(
- searchOptions.indexOf("A only"));
- expect(model.performSearch("A")).andReturn(
- Arrays.asList("some A value", "another A value"));
- view.setDisplayText("[some A value, another A value]");
- replay(model, view);
- presenter.displayViewOnScreen();
-
- presenter.searchButtonPressed();
-
- verify(model, view);
- }
There it too much "setup" code: I notice that this test is doing several things that the previous test did. And I expect that most of the other tests will have to create and initialize the same objects, just as these two tests did. So I will refactor the tests to make them more readable:
- package client;
-
- import static org.easymock.EasyMock.*;
- import java.util.Arrays;
- import java.util.List;
- import junit.framework.TestCase;
- import common.IModel;
- import common.IView;
-
- public class PresenterTest extends TestCase {
-
- private IModel model;
- private IView view;
- private Presenter presenter;
-
- private List<String> searchOptions;
-
- @Override
- protected void setUp() {
- model = createStrictMock(IModel.class);
- view = createStrictMock(IView.class);
- presenter = new Presenter(view, model);
- view.initalizeAndDisplayView(presenter);
-
- searchOptions = Arrays.asList(presenter
- .getSearchOptionSelectionStrings());
- }
-
- private void finishSetup() {
- replay(model, view);
- presenter.displayViewOnScreen();
- }
-
- @Override
- protected void tearDown() throws Exception {
- verify(model, view);
- }
-
- public void testUserVisibleSearchSelectionOptions() {
- finishSetup();
-
- assertEquals(Arrays.asList("A only", "B only", "- ALL -"),
- searchOptions);
- }
-
- public void testAOnly() {
- expect(view.getSelectedSearchIndex()).andReturn(
- searchOptions.indexOf("A only"));
- expect(model.performSearch("A")).andReturn(
- Arrays.asList("some A value", "another A value"));
- view.setDisplayText("[some A value, another A value]");
- finishSetup();
-
- presenter.searchButtonPressed();
- }
- }
In this refactoring I have done the following:
- Extracted object creation to a common
setUp
method, which JUnit will run before each test. - Extracted common end-of-test EasyMock validation logic to the
tearDown
method, whichJUnit will run after each test. - Extracted fetching the list of drop-down selections to the
setUp
method, so that tests can contain the user-visible value that the user would select on the screen rather than an internal index value. - Extracted the standard EasyMock code for the transition from "record" to "playback" modes into a
finishSetup
method.
These changes make the current tests shorter and more readable. And this change will benefit the next few tests as well.
Finishing the Presenter Class
Remembering that the Presenter
is hard-coding the value that it sends to the Model
, rather than doing something sensible based on the user-selected value, I say that we should write a test to force us to implement better production code, and to ensure that this new code is properly tested:
- public void testBOnly() {
- expect(view.getSelectedSearchIndex()).andReturn(
- searchOptions.indexOf("B only"));
- expect(model.performSearch("B")).andReturn(
- Arrays.asList("some B value", "another B value"));
- view.setDisplayText("[some B value, another B value]");
- finishSetup();
-
- presenter.searchButtonPressed();
- }
This fails with the expected EasyMock assertion error:
- java.lang.AssertionError:
- Unexpected method call performSearch("A"):
- performSearch("B"): expected: 1, actual: 0
- ...
- at client.Presenter.searchButtonPressed(Presenter.java:31)
- at client.PresenterTest.testBOnly(PresenterTest.java:66)
This provides us with sufficient reason to finish the implementation of the searchButtonPressed
method, by adding a second array field with the values to send to the Model
for each of the drop-down values the user might select in the View
:
- ...
- public class Presenter {
-
- private static final String[] SEARCH_OPTIONS = new String[] { "A only",
- "B only", "- ALL -" };
- private static final String[] SEARCH_CODES = new String[] { "A", "B" };
-
- ...
-
- public void searchButtonPressed() {
- int selectedSearchIndex = view.getSelectedSearchIndex();
- String searchCode = SEARCH_CODES[selectedSearchIndex];
- List<String> searchResult = search.performSearch(searchCode);
- view.setDisplayText(searchResult.toString());
- }
- }
But we are still not finished: We need to properly handle the "- ALL -"
case, which should return all the values in the database, unfiltered:
- public void testAll() {
- expect(view.getSelectedSearchIndex()).andReturn(
- searchOptions.indexOf("- ALL -"));
- expect(model.performSearch(null)).andReturn(
- Arrays.asList("A value", "B value", "C value"));
- view.setDisplayText("[A value, B value, C value]");
- finishSetup();
-
- presenter.searchButtonPressed();
- }
A java.lang.ArrayIndexOutOfBoundsException
on the index value 2 reminds us that our SEARCH_CODES
array needs to have as many entries as our SEARCH_OPTIONS
array, so we add a null
value, to represent that we want to find all values, not just those for one code:
private static final String[] SEARCH_CODES = new String[] { "A", "B", null };
As a final touch, just to ensure that the system is robust in the face of unexpected errors, I will add a test to ensure that unexpected RuntimeException
objects are handled well:
- public void testModelThrowsRuntimeException() {
- expect(view.getSelectedSearchIndex()).andReturn(
- searchOptions.indexOf("A only"));
- expect(model.performSearch("A")).andThrow(new RuntimeException());
- view.setDisplayText(RuntimeException.class.getName());
- finishSetup();
-
- presenter.searchButtonPressed();
- }
And this gives us a finished Presenter
implementation:
- package client;
-
- import java.util.List;
-
- import common.IModel;
- import common.IView;
-
- public class Presenter {
-
- private static final String[] SEARCH_OPTIONS = new String[] { "A only",
- "B only", "- ALL -" };
- private static final String[] SEARCH_CODES = new String[] { "A", "B", null };
-
- private final IView view;
- private final IModel search;
-
- public Presenter(IView view, IModel search) {
- this.view = view;
- this.search = search;
- }
-
- public void displayViewOnScreen() {
- view.initalizeAndDisplayView(this);
- }
-
- public String[] getSearchOptionSelectionStrings() {
- return SEARCH_OPTIONS;
- }
-
- public void searchButtonPressed() {
- try {
- int selectedSearchIndex = view.getSelectedSearchIndex();
- String searchCode = SEARCH_CODES[selectedSearchIndex];
- List<String> searchResult = search.performSearch(searchCode);
- view.setDisplayText(searchResult.toString());
- } catch (RuntimeException runtimeException) {
- view.setDisplayText(runtimeException.getClass().getName());
- }
- }
- }
Developing the Application Model and Database
We have verified that the Presenter
class coordinates the actions of the application's View
and Model
. For the next step, we will move forward with testing and implementing a Model
class for this application:
First Model Test
As before, we start with a test:
- package server;
-
- import static org.easymock.EasyMock.*;
- import java.util.*;
- import common.IDatabase;
- import junit.framework.TestCase;
-
- public class ModelTest extends TestCase {
-
- public void testAOnly() {
- IDatabase database = createStrictMock(IDatabase.class);
- expect(database.getData()).andReturn(new String[] {"valueA", "valueB"}).anyTimes();
- replay(database);
- Model model = new Model(database);
-
- List<String> searchResults = model.performSearch("A");
-
- assertEquals(Arrays.asList("valueA"), searchResults);
- verify(database);
- }
- }
For this to compile and pass the test, we need an IDatabase
interface:
- package common;
-
- public interface IDatabase {
- String[] getData();
- }
And we need a minimal implementation of the Model
class:
- package server;
-
- import java.util.*;
- import common.*;
-
- public class Model implements IModel {
-
- public Model(IDatabase database) {
- }
-
- public List<String> performSearch(String searchValue) {
- return Arrays.asList("valueA");
- }
- }
As before, we elect to hard-code a value ("valueA"
) to make this test pass, knowing that we need to add more tests to justify and fully test the intended final implementation.
Second Model Test
Recognizing that we need another test, we write one:
- public void testBOnly() {
- IDatabase database = createStrictMock(IDatabase.class);
- expect(database.getData()).andReturn(new String[] {"valueA", "valueB"}).anyTimes();
- replay(database);
- Model model = new Model(database);
-
- List<String> searchResults = model.performSearch("B");
-
- assertEquals(Arrays.asList("valueB"), searchResults);
- verify(database);
- }
And this gives us sufficient reason to finish out the Model
implementation:
- package server;
-
- import java.util.*;
- import common.*;
-
- /**
- * Search model: Performs searches for records matching the last character of
- * each database record.
- */
- public class Model implements IModel {
-
- private final IDatabase database;
-
- public Model(IDatabase database) {
- this.database = database;
- }
-
- public List<String> performSearch(String searchValue) {
- ArrayList<String> result = new ArrayList<String>();
- String[] records = database.getData();
- for (int dbIdx = 0; dbIdx < records.length; ++dbIdx) {
- String record = records[dbIdx];
- if (record.endsWith(searchValue)) {
- result.add(record);
- }
- }
- return result;
- }
- }
Because of the way the data is structured in this implementation, searching for all records "of type A" can most easily and simply be done by finding all records that end with the string value "A".
Refactoring the Tests
As before, we notice a fair amount of duplication between the tests, so we refactor it out to setUp
and tearDown
methods, and the appropriate fields:
- package server;
-
- import static org.easymock.EasyMock.*;
- import java.util.*;
- import common.IDatabase;
- import junit.framework.TestCase;
-
- public class ModelTest extends TestCase {
-
- private Model model;
- private IDatabase database;
-
- @Override
- protected void setUp() throws Exception {
- database = createStrictMock(IDatabase.class);
- expect(database.getData()).andReturn(new String[] {"valueA", "valueB"}).anyTimes();
- replay(database);
-
- model = new Model(database);
- }
-
- @Override
- protected void tearDown() throws Exception {
- verify(database);
- }
-
- public void testAOnly() {
-
- List<String> searchResults = model.performSearch("A");
-
- assertEquals(Arrays.asList("valueA"), searchResults);
- }
-
- public void testBOnly() {
-
- List<String> searchResults = model.performSearch("B");
-
- assertEquals(Arrays.asList("valueB"), searchResults);
- }
- }
Putting all the EasyMock setup logic in the setUp
method simplifies these tests, and makes writing new tests easier, as we will soon see...
Finishing the Model Tests
We want to ensure that the model performs properly when given invalid values, so we write a test for this:
- public void testBadSearch() {
-
- String badSearchValue = "X";
- List<String> searchResults = model.performSearch(badSearchValue);
-
- assertEquals(Collections.EMPTY_LIST, searchResults);
- }
Likewise, we need to provide a way to search for all records without filtering them. And we notice that the easiest way to do this is to pass an empty string as the search criteria, as all strings end with the empty string:
- public void testAll() {
- String noSearchRestriction = "";
-
- List<String> searchResults = model.performSearch(noSearchRestriction);
-
- assertEquals(Arrays.asList("valueA", "valueB"), searchResults);
- }
At this point we see that the Model
object passes all of our tests, and I am satisfied that this is a good implementation, so we can move on to the other components of the system.
Integrating the System Components with Spring
We have unit tested the Presenter
and Model
components to the point of 100% code coverage. We have even tested that both properly handle boundary conditions, bad data, and exceptions. So we can reasonably have confidence that when we integrate these components with the Graphical User Interface (GUI), it will work well.
The Main Method
We would like to integrate this application using the Dependency Injection functionality of the Spring Library. So we will start with a simple and direct Java class that ensures that the system is running on the Swing GUI thread, as required by Swing:
- import javax.swing.SwingUtilities;
-
- import org.springframework.context.support.AbstractApplicationContext;
- import org.springframework.context.support.ClassPathXmlApplicationContext;
-
- import client.Presenter;
-
- public class MainJavaApplication implements Runnable {
-
- public static void main(String[] args) {
- SwingUtilities.invokeLater(new MainJavaApplication());
- }
-
- @Override
- public void run() {
- AbstractApplicationContext applicationContext =
- new ClassPathXmlApplicationContext("context.xml");
- Presenter presenter =
- (Presenter) applicationContext.getBean("presenter");
- presenter.displayViewOnScreen();
- }
- }
Running, this, we note that we need a Spring configuration file, so we provide one:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
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-2.0.xsd">
<bean id="presenter" class="client.Presenter" >
<constructor-arg ref="view" />
<constructor-arg ref="model" />
</bean>
<bean id="view" class="client.View" />
<bean id="model" class="server.Model">
<constructor-arg ref="database" />
</bean>
<bean id="database" class="server.Database" />
</beans>
Running this reveals that we need a client.View
class. The tools required to do automated testing of Swing components are beyond the scope of this article, so we will just write the code and test it by hand:
- package client;
-
- import java.awt.*;
- import java.awt.event.*;
- import javax.swing.*;
- import common.*;
-
- public class View implements IView {
-
- private JFrame frame;
- public JComboBox comboBox;
- public JTextArea textArea;
-
- @Override
- public void initalizeAndDisplayView(final Presenter presenter) {
- frame = new JFrame("Nifty Search Application");
- frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
- JPanel pane = new JPanel();
- comboBox = new JComboBox(presenter.getSearchOptionSelectionStrings());
- JButton searchButton = new JButton("Search");
- textArea = new JTextArea();
-
- searchButton.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent actionEvent) {
- presenter.searchButtonPressed();
- }
- });
-
- pane.add(comboBox);
- pane.add(searchButton);
- pane.add(textArea);
-
- frame.add(pane, BorderLayout.CENTER);
- frame.pack();
- frame.setVisible(true);
- }
-
- @Override
- public int getSelectedSearchIndex() {
- return comboBox.getSelectedIndex();
- }
-
- @Override
- public void setDisplayText(String displayText) {
- textArea.setText(displayText);
- frame.pack();
- }
- }
This almost works -- except that we need a server.Database
class. In a real application, this class would use a relational database, web services, or some other form of storage. But for this example application we will implement the simplest possible internal "database," as shown below:
- package server;
-
- import common.IDatabase;
-
- public class Database implements IDatabase {
-
- public static String[] DATABASE_OF_VALUES = { //
- //
- "firstA", "firstB", //
- "secondA", "secondB", //
- "thirdA", "thirdB", //
- };
-
- @Override
- public String[] getData() {
- return Database.DATABASE_OF_VALUES;
- }
- }
Now it runs successfully.
Success!
The application runs, and produces the expected startup screen:
The drop-down list box looks good:
When searching for "A" records, we get the expected result:
When searching for "B" records, we get the expected result:
When searching for all records, without filtering, the NullPointerException
is not the result we wanted.
So what went wrong?
Fixing the Bug that the Tests Did Not Catch
We have 100% code coverage, in the classes we tested, so how did we miss this bug?
Some people might jump to the conclusion that the bug must in the code we did not test. On the other hand, it might be wise to not be so hasty...
Tracking Down the BUG Using a Debugger
We know that the NullPointerException
occurs when we select "- ALL -" in the drop-down list box, and then press the "Search" button. So I set a breakpoint on the first line of application code that executes when the "Search" button is pressed -- the "presenter.searchButtonPressed();
" call in the View
class.
Clicking on the MainJavaApplication
class with the right mouse button, I chose "Debug As" -> "Java Application", to run it in the debugger. Selecting "- ALL -"
in the drop-down list box, and pressing the "Search" button causes the debugger to stop at the breakpoint we set above.
Stepping into the Presenter
code, and then continuing to step, line by line, we find that...
selectedSearchIndex = 2
, as expectedsearchCode = null
, as expressed inPresenterTest
. (ThetestAll
method in that class contains the code 'expect(model.performSearch(null)).andReturn(...);
'.)- But when we step into the
Model
code, where the system tries to evaluate the lineif (record.endsWith(searchValue)) {
with the valuesrecord = "firstA"
searchValue = null
NullPointerException
.
This bug reveals the conflict between the following two tests:
- ...
- public class PresenterTest extends TestCase {
- ...
- public void testAll() {
- expect(view.getSelectedSearchIndex()).andReturn(
- searchOptions.indexOf("- ALL -"));
- expect(model.performSearch(<u>null</u>)).andReturn(
- Arrays.asList("A value", "B value", "C value"));
- view.setDisplayText("[A value, B value, C value]");
- finishSetup();
-
- presenter.searchButtonPressed();
- }
- ...
- }
and
- ...
- public class ModelTest extends TestCase {
- ...
- public void testAll() {
- String noSearchRestriction = <u>""</u>;
-
- List<String> searchResults = model.performSearch(noSearchRestriction);
-
- assertEquals(Arrays.asList("valueA", "valueB"), searchResults);
- }
- }
The problem is that when writing the Presenter
tests, we thought that null
would be a good value to represent "all possible values." But when we got down into the implementation of the Model
object, we found that using an empty string would be much more convenient. Certainly we can correct the problem by correcting the tests. But a more important question is how to prevent this kind of problem from happening in the future.
If unit testing is not sufficient to ensure that the system will run correctly, then what should we do instead?
Also, it is a generally recognized best practice for fixing bugs is to first, find a way to produce the buggy behavior on demand. Second, fix the bug. Then third, test the program to verify that the bug is now fixed.
Automated Regression Testing is likely to prove useful here. And when unit testing is insufficient, we will need to do integration testing.
Integration Testing with the Spring Framework
To ensure that the classes we write will work together correctly when we wire them together using the Spring library, we need to test them after the Spring library has wired them together. Otherwise, we cannot be sure that our Spring configuration is correct, and that the classes will work together as expected.
How To Test Spring Wired Components
The Spring Framework provides a number of JUnit TestCase
classes intended to help you write JUnit tests. A full list is beyond the scope of this article. The most useful one for our purposes is the AbstractDependencyInjectionSpringContextTests
class, which loads and configures a Spring context for us, so that we can test the objects it creates.
Integration Testing with Spring
Diving right into it, we create a test class that extends Spring's AbstractDependencyInjectionSpringContextTests
class, creating this integration test:
- package integration;
-
- import javax.swing.JComboBox;
- import org.springframework.test.AbstractDependencyInjectionSpringContextTests;
- import client.Presenter;
- import client.View;
- import common.IDatabase;
-
- public class IntegrationTest extends
- AbstractDependencyInjectionSpringContextTests {
-
- protected View view;
- protected IDatabase database;
- protected Presenter presenter;
-
- public IntegrationTest() throws Exception {
- setPopulateProtectedVariables(true);
- }
-
- @Override
- protected String[] getConfigLocations() {
- return new String[] { "/context.xml" };
- }
-
- public void testSearchAOnly() {
- presenter.displayViewOnScreen();
-
- selectItem(view.comboBox, "A only");
- presenter.searchButtonPressed();
-
- assertEquals("[firstA, secondA, thirdA]", view.textArea.getText());
- }
-
- public void testSearchBOnly() {
- presenter.displayViewOnScreen();
-
- selectItem(view.comboBox, "B only");
- presenter.searchButtonPressed();
-
- assertEquals("[firstB, secondB, thirdB]", view.textArea.getText());
- }
-
- private static void selectItem(JComboBox comboBox, String itemValue) {
- comboBox.setSelectedItem(itemValue);
- assertEquals(itemValue, comboBox.getSelectedItem());
- }
- }
This is an excellent example of an integration test: It tests everything in the application, including displaying the Swing Graphical User Interface (GUI) while it runs, and fetching data from the "live production database."
And that is the problem:
- Every time we run the tests, a flurry of live screens flit across the computer's screen, making it impossible to do any other work on the workstation while the tests run. For this application this is not an issue. But in a realistically sized application, locking up a developer's work station every time they run tests will be very costly and distracting.
- The tests cannot easily control the data in an external database or web service, so some tests will be nearly impossible to write. And others will fail from time to time as the data in the database may change.
To resolve these problems we need to have a way to do integration testing with Spring, and yet also be able to mock some Spring created objects, so that we can control the test environment.
Integration Testing with Spring and EasyMock Together
Split the Spring Configuration Files
To populate some of the Spring-created objects with mock objects, we need to split our Spring configuration file so that the Spring beans we want to populate differently are in a separate Spring configuration file. We will do this by renaming the context.xml
file to be main-context.xml
, and creating a new Spring configuration file called additional-context.xml
. We want to mock out the View
and Database
objects, so we move their bean definitions to the additional-context.xml
file.
The main-context.xml
file now looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
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-2.0.xsd">
<bean id="presenter" class="client.Presenter" >
<constructor-arg ref="view" />
<constructor-arg ref="model" />
</bean>
<bean id="model" class="server.Model">
<constructor-arg ref="database" />
</bean>
</beans>
And the additional-context.xml
file now looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
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-2.0.xsd">
<bean id="view" class="client.View" />
<bean id="database" class="server.Database" />
</beans>
The MainJavaApplication
has a single change, from this:
AbstractApplicationContext applicationContext =
new ClassPathXmlApplicationContext("context.xml");
to this:
AbstractApplicationContext applicationContext = new ClassPathXmlApplicationContext(
new String[] { "main-context.xml", "additional-context.xml" });
and our IntegrationTest
uses the two context files, instead of just one:
@Override
protected String[] getConfigLocations() {
return new String[] { "/main-context.xml", "/additional-context.xml" };
}
Now we have better control over the Spring configuration, but we have not changed fixed the bug -- yet.
Mock the View and Database
To provide alternate implementations of for the View
and Database
objects, we will provide an alternate test Spring configuration file, test-context.xml
, to replace the additional-context.xml
file. And it will provide EasyMock objects to stand in for the View
and Database
objects.
So we change our IntegrationTest
class once more, to use test-context.xml
, instead of additional-context.xml
:
@Override
protected String[] getConfigLocations() {
return new String[] { "/main-context.xml", "/integration/test-context.xml" };
}
And we create the test-context.xml
file, using EasyMock to create each of the objects:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
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-2.0.xsd">
<bean id="view" class="org.easymock.EasyMock" factory-method="createStrictMock">
<constructor-arg value="common.IView" ></constructor>
</bean>
<bean id="database" class="org.easymock.EasyMock" factory-method="createStrictMock">
<constructor-arg value="common.IDatabase" ></constructor>
</bean>
</beans>
Then we update the IntegrationTest
class to properly initialize, use and verify the mock objects:
- package integration;
-
- import static org.easymock.EasyMock.*;
- import java.util.Arrays;
- import java.util.List;
- import org.springframework.test.AbstractDependencyInjectionSpringContextTests;
- import client.Presenter;
- import common.IDatabase;
- import common.IView;
-
- public class IntegrationTest extends
- AbstractDependencyInjectionSpringContextTests {
-
- protected IView view;
- protected IDatabase database;
- protected Presenter presenter;
- private List<String> searchOptions;
-
- public IntegrationTest() throws Exception {
- setPopulateProtectedVariables(true);
- }
-
- @Override
- protected String[] getConfigLocations() {
- return new String[] { "/main-context.xml",
- "/integration/test-context.xml" };
- }
-
- @Override
- protected void onSetUp() throws Exception {
- reset(view, database);
- view.initalizeAndDisplayView(presenter);
- expect(database.getData()).andReturn(
- new String[] { "valueA", "valueB" }).anyTimes();
-
- searchOptions = Arrays.asList(presenter
- .getSearchOptionSelectionStrings());
- }
-
- private void doFinishSetup() {
- replay(view, database);
- presenter.displayViewOnScreen();
- }
-
- @Override
- protected void onTearDown() throws Exception {
- verify(view, database);
- }
-
- public void testSearchAOnly() {
- expect(view.getSelectedSearchIndex()).andReturn(
- searchOptions.indexOf("A only"));
- view.setDisplayText("[valueA]");
- doFinishSetup();
-
- presenter.searchButtonPressed();
- }
-
- public void testSearchBOnly() {
- expect(view.getSelectedSearchIndex()).andReturn(
- searchOptions.indexOf("B only"));
- view.setDisplayText("[valueB]");
- doFinishSetup();
-
- presenter.searchButtonPressed();
- }
-
- public void testSearchAll() {
- expect(view.getSelectedSearchIndex()).andReturn(
- searchOptions.indexOf("- ALL -"));
- view.setDisplayText("[valueA, valueB]");
- doFinishSetup();
-
- presenter.searchButtonPressed();
- }
- }
Fixing the Bug
The last test above, testSearchAll
illustrates the integration error we are trying to fix, by failing with this error:
- java.lang.AssertionError:
- Unexpected method call setDisplayText("java.lang.NullPointerException"):
- getSelectedSearchIndex(): expected: 1, actual: 1
- setDisplayText("[valueA, valueB]"): expected: 1, actual: 0
- ...
The error message says that while we were expecting to see the value "[valueA, valueB]"
on the display, we instead saw the value "java.lang.NullPointerException"
. Now that we have a test that illustrates the error, we can fix it.
This is easily done by changing the null
in the Presenter
class to an empty string value (IE:""
), like this:
private static final String[] SEARCH_CODES = new String[] { "A", "B", "" };
Having done that, we find that we also need to change the test method in PresenterTest
to match the assumption made in ModelTest
:
- public void testAll() {
- expect(view.getSelectedSearchIndex()).andReturn(
- searchOptions.indexOf("- ALL -"));
- expect(model.performSearch("")).andReturn(
- Arrays.asList("A value", "B value", "C value"));
- view.setDisplayText("[A value, B value, C value]");
- finishSetup();
-
- presenter.searchButtonPressed();
- }
Successful Search
So now, at last, the "- ALL -" search works correctly:
How did we do it?
To do integration testing with real Spring configuration files, with EasyMock substitutes for some Spring beans, we did these things:
- We extended Spring's
AbstractDependencyInjectionSpringContextTests
to create our JUnit test. - We split our Spring configuration file, separating out the beans we wished to mock into a separate file. (
additional-context.xml
) - We had our tests provide an alternate Spring configuration file that creates EasyMock objects, using Spring's ability to call static factory methods. (See the
test-context.xml
file, above.) - In the tests, we used EasyMock's
reset
function to clear the mock objects back to their original state, and then used method calls, andreplay
andverify
functions to specify and verify correct behavior.
Reflection
So what did we learn?
We learned that...
- The Test-Driven Development method gave us a reliable program that was easy and safe to change, but that it was not sufficient to ensure correct operation of the finished program unless integration testing was done.
- But we also saw that we can automate integration tests.
- We can do automated testing with Spring-wired components, and still inject test-specific objects and behavior where needed.
Here is something else to consider: We found that we need integration tests to ensure that the system as a whole produces the correct results. But now that we have integration tests, we can see that some of our unit tests are now redundant.
For example, consider the IntegrationTest
for the "search all" functionality:
- ...
- expect(database.getData()).andReturn(new String[] { "valueA", "valueB" }).anyTimes();
- ...
-
- public void testSearchAll() {
- expect(view.getSelectedSearchIndex()).andReturn(
- searchOptions.indexOf("- ALL -"));
- view.setDisplayText("[valueA, valueB]");
- doFinishSetup();
-
- presenter.searchButtonPressed();
- }
Given that the database contains the values "valueA" and "valueB", a search for all values displays the string value "[valueA, valueB]"
.
This is similar to the ModelTest
to search all:
- ...
- expect(database.getData()).andReturn(new String[] {"valueA", "valueB"}).anyTimes();
- ...
-
- public void testAll() {
- String noSearchRestriction = "";
-
- List<String> searchResults = model.performSearch(noSearchRestriction);
-
- assertEquals(Arrays.asList("valueA", "valueB"), searchResults);
- }
And it is similar to the PresenterTest
to search all, which could be written like this:
- public void testAll() {
- expect(view.getSelectedSearchIndex()).andReturn(searchOptions.indexOf("- ALL -"));
- expect(model.performSearch("")).andReturn(Arrays.asList("valueA", "valueB"));
- view.setDisplayText("[valueA, valueB]");
- finishSetup();
-
- presenter.searchButtonPressed();
- }
The main difference between the integration test and the unit tests is that the unit tests use the empty string to trigger the "search all" functionality, while the integration test does not need to specify the design and behavior of the program to this level of detail. Doing this makes the automated regression tests less sensitive to the low-level design details, making it easier to refactor the production code without having to change the tests.
Notice that the model and presenter versions of the testAll
method both test code that eventually was covered by the testSearchAll
integration test method. So when we know that integration testing will be needed, and we have made the investment necessary to do it, we could probably save time and money by writing the integration test first, and then we will not need to write equivalent unit tests for the same functionality.
Summary
- We have developed a simple Java Swing program using the Test Driven Development practice.
- We have implemented automated regression tests of a "Spring Framework wired application" using JUnit and EasyMock libraries.
- We have considered the benefits of putting more emphasis on integration testing, rather than focusing exclusively on unit testing.
Now I encourage you to go out and write your own programs using these tools and techniques, in the hopes that all of our systems may be better structured, more maintainable and with fewer bugs.
References
The Example Program
- [1] Source Code
Dec2009-Final Source Code.zip
Glossary
- [2] Class Diagram
http://en.wikipedia.org/wiki/Class_diagram - [3] Data Access Object
http://en.wikipedia.org/wiki/Data_access_object - [4] Dependency Injection
http://en.wikipedia.org/wiki/Dependency_Injection - [5] Graphical User Interface
http://en.wikipedia.org/wiki/Graphical_User_Interface - [6] Interface Based Programming
http://en.wikipedia.org/wiki/Interface_based_programming - [7] Mock Object
http://en.wikipedia.org/wiki/Mock_object - [8] Model View Presenter
http://en.wikipedia.org/wiki/Model-view-presenter - [9] Regression Testing
http://en.wikipedia.org/wiki/Regression_testing - [10] Test Driven Development
http://en.wikipedia.org/wiki/Test-driven_development
Libraries
- [11] EasyMock, a Mock Object library for Java
http://easymock.org/ - [12] JUnit, for Java Automated Regression Testing
http://junit.org/ - [13] The Spring Framework, from Spring Source
http://www.springsource.org/about - [14] JavaDoc for Class AbstractDependencyInjectionSpringContextTests
http://static.springsource.org/spring/docs/2.5.x/api/org/springframework/test/AbstractDependencyInjectionSpringContextTests.html