An Example of Test Driven Development with the Spring Framework

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 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:

Drop Down MenuSearch Result

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:

Diagram

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...

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:

  1. package client;
  2.  
  3. import static org.easymock.EasyMock.createStrictMock;
  4. import static org.easymock.EasyMock.replay;
  5. import static org.easymock.EasyMock.verify;
  6.  
  7. import java.util.Arrays;
  8.  
  9. import junit.framework.TestCase;
  10.  
  11. import common.IModel;
  12. import common.IView;
  13.  
  14. public class PresenterTest extends TestCase {
  15.  
  16. public void testUserVisibleSearchSelectionOptions() {
  17. IModel model = createStrictMock(IModel.class);
  18. IView view = createStrictMock(IView.class);
  19. Presenter presenter = new Presenter(view model);
  20. replay(model, view);
  21.  
  22. String[] searchOptionSelectionStrings = presenter
  23. .getSearchOptionSelectionStrings();
  24.  
  25. assertEquals(Arrays.asList("A only", "B only", "- ALL -"), //
  26. Arrays.asList(searchOptionSelectionStrings));
  27. verify(model, view);
  28. }
  29. }

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...

This gives us the following two interface classes:

  1. package common;
  2.  
  3. public interface IModel {
  4. }
  1. package common;
  2.  
  3. public interface IView {
  4. }

...and this Presenter class:

  1. package client;
  2.  
  3. import common.IModel;
  4. import common.IView;
  5.  
  6. public class Presenter {
  7.  
  8. private static final String[] SEARCH_OPTIONS = new String[] { "A only",
  9. "B only", "- ALL -" };
  10.  
  11. public Presenter(IView view, IModel search) {
  12. }
  13.  
  14. public String[] getSearchOptionSelectionStrings() {
  15. return SEARCH_OPTIONS;
  16. }
  17. }

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:

  1. public void testAOnly() {
  2. IModel model = createStrictMock(IModel.class);
  3. IView view = createStrictMock(IView.class);
  4. Presenter presenter = new Presenter(view, model);
  5. view.initalizeAndDisplayView(presenter);
  6. List<String> searchOptions = Arrays.asList(presenter
  7. .getSearchOptionSelectionStrings());
  8. expect(view.getSelectedSearchIndex()).andReturn(
  9. searchOptions.indexOf("A only"));
  10. expect(model.performSearch("A")).andReturn(
  11. Arrays.asList("some A value", "another A value"));
  12. view.setDisplayText("[some A value, another A value]");
  13. replay(model, view);
  14. presenter.displayViewOnScreen();
  15.  
  16. presenter.searchButtonPressed();
  17.  
  18. verify(model, view);
  19. }

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:

  1. package client;
  2.  
  3. import java.util.List;
  4.  
  5. import common.IModel;
  6. import common.IView;
  7.  
  8. public class Presenter {
  9.  
  10. private static final String[] SEARCH_OPTIONS = new String[] { "A only",
  11. "B only", "- ALL -" };
  12.  
  13. private final IView view;
  14. private final IModel search;
  15.  
  16. public Presenter(IView view, IModel search) {
  17. this.view = view;
  18. this.search = search;
  19. }
  20.  
  21. public void displayViewOnScreen() {
  22. view.initalizeAndDisplayView(this);
  23. }
  24.  
  25. public String[] getSearchOptionSelectionStrings() {
  26. return SEARCH_OPTIONS;
  27. }
  28.  
  29. public void searchButtonPressed() {
  30. int selectedSearchIndex = view.getSelectedSearchIndex();
  31. List<String> searchResult = search.performSearch("A");
  32. view.setDisplayText(searchResult.toString());
  33. }
  34. }

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:

  1. public void testAOnly() {
  2. IModel model = createStrictMock(IModel.class);
  3. IView view = createStrictMock(IView.class);
  4. Presenter presenter = new Presenter(view, model);
  5. view.initalizeAndDisplayView(presenter);
  6. List<String> searchOptions = Arrays.asList(presenter
  7. .getSearchOptionSelectionStrings());
  8. expect(view.getSelectedSearchIndex()).andReturn(
  9. searchOptions.indexOf("A only"));
  10. expect(model.performSearch("A")).andReturn(
  11. Arrays.asList("some A value", "another A value"));
  12. view.setDisplayText("[some A value, another A value]");
  13. replay(model, view);
  14. presenter.displayViewOnScreen();
  15.  
  16. presenter.searchButtonPressed();
  17.  
  18. verify(model, view);
  19. }

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:

  1. package client;
  2.  
  3. import static org.easymock.EasyMock.*;
  4. import java.util.Arrays;
  5. import java.util.List;
  6. import junit.framework.TestCase;
  7. import common.IModel;
  8. import common.IView;
  9.  
  10. public class PresenterTest extends TestCase {
  11.  
  12. private IModel model;
  13. private IView view;
  14. private Presenter presenter;
  15.  
  16. private List<String> searchOptions;
  17.  
  18. @Override
  19. protected void setUp() {
  20. model = createStrictMock(IModel.class);
  21. view = createStrictMock(IView.class);
  22. presenter = new Presenter(view, model);
  23. view.initalizeAndDisplayView(presenter);
  24.  
  25. searchOptions = Arrays.asList(presenter
  26. .getSearchOptionSelectionStrings());
  27. }
  28.  
  29. private void finishSetup() {
  30. replay(model, view);
  31. presenter.displayViewOnScreen();
  32. }
  33.  
  34. @Override
  35. protected void tearDown() throws Exception {
  36. verify(model, view);
  37. }
  38.  
  39. public void testUserVisibleSearchSelectionOptions() {
  40. finishSetup();
  41.  
  42. assertEquals(Arrays.asList("A only", "B only", "- ALL -"),
  43. searchOptions);
  44. }
  45.  
  46. public void testAOnly() {
  47. expect(view.getSelectedSearchIndex()).andReturn(
  48. searchOptions.indexOf("A only"));
  49. expect(model.performSearch("A")).andReturn(
  50. Arrays.asList("some A value", "another A value"));
  51. view.setDisplayText("[some A value, another A value]");
  52. finishSetup();
  53.  
  54. presenter.searchButtonPressed();
  55. }
  56. }

In this refactoring I have done the following:

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:

  1. public void testBOnly() {
  2. expect(view.getSelectedSearchIndex()).andReturn(
  3. searchOptions.indexOf("B only"));
  4. expect(model.performSearch("B")).andReturn(
  5. Arrays.asList("some B value", "another B value"));
  6. view.setDisplayText("[some B value, another B value]");
  7. finishSetup();
  8.  
  9. presenter.searchButtonPressed();
  10. }

This fails with the expected EasyMock assertion error:

  1. java.lang.AssertionError:
  2. Unexpected method call performSearch("A"):
  3. performSearch("B"): expected: 1, actual: 0
  4. ...
  5. at client.Presenter.searchButtonPressed(Presenter.java:31)
  6. 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:

  1. ...
  2. public class Presenter {
  3.  
  4. private static final String[] SEARCH_OPTIONS = new String[] { "A only",
  5. "B only", "- ALL -" };
  6. private static final String[] SEARCH_CODES = new String[] { "A", "B" };
  7.  
  8. ...
  9.  
  10. public void searchButtonPressed() {
  11. int selectedSearchIndex = view.getSelectedSearchIndex();
  12. String searchCode = SEARCH_CODES[selectedSearchIndex];
  13. List<String> searchResult = search.performSearch(searchCode);
  14. view.setDisplayText(searchResult.toString());
  15. }
  16. }

But we are still not finished: We need to properly handle the "- ALL -" case, which should return all the values in the database, unfiltered:

  1. public void testAll() {
  2. expect(view.getSelectedSearchIndex()).andReturn(
  3. searchOptions.indexOf("- ALL -"));
  4. expect(model.performSearch(null)).andReturn(
  5. Arrays.asList("A value", "B value", "C value"));
  6. view.setDisplayText("[A value, B value, C value]");
  7. finishSetup();
  8.  
  9. presenter.searchButtonPressed();
  10. }

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:

  1. public void testModelThrowsRuntimeException() {
  2. expect(view.getSelectedSearchIndex()).andReturn(
  3. searchOptions.indexOf("A only"));
  4. expect(model.performSearch("A")).andThrow(new RuntimeException());
  5. view.setDisplayText(RuntimeException.class.getName());
  6. finishSetup();
  7.  
  8. presenter.searchButtonPressed();
  9. }

And this gives us a finished Presenter implementation:

  1. package client;
  2.  
  3. import java.util.List;
  4.  
  5. import common.IModel;
  6. import common.IView;
  7.  
  8. public class Presenter {
  9.  
  10. private static final String[] SEARCH_OPTIONS = new String[] { "A only",
  11. "B only", "- ALL -" };
  12. private static final String[] SEARCH_CODES = new String[] { "A", "B", null };
  13.  
  14. private final IView view;
  15. private final IModel search;
  16.  
  17. public Presenter(IView view, IModel search) {
  18. this.view = view;
  19. this.search = search;
  20. }
  21.  
  22. public void displayViewOnScreen() {
  23. view.initalizeAndDisplayView(this);
  24. }
  25.  
  26. public String[] getSearchOptionSelectionStrings() {
  27. return SEARCH_OPTIONS;
  28. }
  29.  
  30. public void searchButtonPressed() {
  31. try {
  32. int selectedSearchIndex = view.getSelectedSearchIndex();
  33. String searchCode = SEARCH_CODES[selectedSearchIndex];
  34. List<String> searchResult = search.performSearch(searchCode);
  35. view.setDisplayText(searchResult.toString());
  36. } catch (RuntimeException runtimeException) {
  37. view.setDisplayText(runtimeException.getClass().getName());
  38. }
  39. }
  40. }

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:

  1. package server;
  2.  
  3. import static org.easymock.EasyMock.*;
  4. import java.util.*;
  5. import common.IDatabase;
  6. import junit.framework.TestCase;
  7.  
  8. public class ModelTest extends TestCase {
  9.  
  10. public void testAOnly() {
  11. IDatabase database = createStrictMock(IDatabase.class);
  12. expect(database.getData()).andReturn(new String[] {"valueA", "valueB"}).anyTimes();
  13. replay(database);
  14. Model model = new Model(database);
  15.  
  16. List<String> searchResults = model.performSearch("A");
  17.  
  18. assertEquals(Arrays.asList("valueA"), searchResults);
  19. verify(database);
  20. }
  21. }

For this to compile and pass the test, we need an IDatabase interface:

  1. package common;
  2.  
  3. public interface IDatabase {
  4. String[] getData();
  5. }

And we need a minimal implementation of the Model class:

  1. package server;
  2.  
  3. import java.util.*;
  4. import common.*;
  5.  
  6. public class Model implements IModel {
  7.  
  8. public Model(IDatabase database) {
  9. }
  10.  
  11. public List<String> performSearch(String searchValue) {
  12. return Arrays.asList("valueA");
  13. }
  14. }

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:

  1. public void testBOnly() {
  2. IDatabase database = createStrictMock(IDatabase.class);
  3. expect(database.getData()).andReturn(new String[] {"valueA", "valueB"}).anyTimes();
  4. replay(database);
  5. Model model = new Model(database);
  6.  
  7. List<String> searchResults = model.performSearch("B");
  8.  
  9. assertEquals(Arrays.asList("valueB"), searchResults);
  10. verify(database);
  11. }

And this gives us sufficient reason to finish out the Model implementation:

  1. package server;
  2.  
  3. import java.util.*;
  4. import common.*;
  5.  
  6. /**
  7.   * Search model: Performs searches for records matching the last character of
  8.   * each database record.
  9.   */
  10. public class Model implements IModel {
  11.  
  12. private final IDatabase database;
  13.  
  14. public Model(IDatabase database) {
  15. this.database = database;
  16. }
  17.  
  18. public List<String> performSearch(String searchValue) {
  19. ArrayList<String> result = new ArrayList<String>();
  20. String[] records = database.getData();
  21. for (int dbIdx = 0; dbIdx < records.length; ++dbIdx) {
  22. String record = records[dbIdx];
  23. if (record.endsWith(searchValue)) {
  24. result.add(record);
  25. }
  26. }
  27. return result;
  28. }
  29. }

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:

  1. package server;
  2.  
  3. import static org.easymock.EasyMock.*;
  4. import java.util.*;
  5. import common.IDatabase;
  6. import junit.framework.TestCase;
  7.  
  8. public class ModelTest extends TestCase {
  9.  
  10. private Model model;
  11. private IDatabase database;
  12.  
  13. @Override
  14. protected void setUp() throws Exception {
  15. database = createStrictMock(IDatabase.class);
  16. expect(database.getData()).andReturn(new String[] {"valueA", "valueB"}).anyTimes();
  17. replay(database);
  18.  
  19. model = new Model(database);
  20. }
  21.  
  22. @Override
  23. protected void tearDown() throws Exception {
  24. verify(database);
  25. }
  26.  
  27. public void testAOnly() {
  28.  
  29. List<String> searchResults = model.performSearch("A");
  30.  
  31. assertEquals(Arrays.asList("valueA"), searchResults);
  32. }
  33.  
  34. public void testBOnly() {
  35.  
  36. List<String> searchResults = model.performSearch("B");
  37.  
  38. assertEquals(Arrays.asList("valueB"), searchResults);
  39. }
  40. }

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:

  1. public void testBadSearch() {
  2.  
  3. String badSearchValue = "X";
  4. List<String> searchResults = model.performSearch(badSearchValue);
  5.  
  6. assertEquals(Collections.EMPTY_LIST, searchResults);
  7. }

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:

  1. public void testAll() {
  2. String noSearchRestriction = "";
  3.  
  4. List<String> searchResults = model.performSearch(noSearchRestriction);
  5.  
  6. assertEquals(Arrays.asList("valueA", "valueB"), searchResults);
  7. }

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:

  1. import javax.swing.SwingUtilities;
  2.  
  3. import org.springframework.context.support.AbstractApplicationContext;
  4. import org.springframework.context.support.ClassPathXmlApplicationContext;
  5.  
  6. import client.Presenter;
  7.  
  8. public class MainJavaApplication implements Runnable {
  9.  
  10. public static void main(String[] args) {
  11. SwingUtilities.invokeLater(new MainJavaApplication());
  12. }
  13.  
  14. @Override
  15. public void run() {
  16. AbstractApplicationContext applicationContext =
  17. new ClassPathXmlApplicationContext("context.xml");
  18. Presenter presenter =
  19. (Presenter) applicationContext.getBean("presenter");
  20. presenter.displayViewOnScreen();
  21. }
  22. }

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:

 
  1. package client;
  2.  
  3. import java.awt.*;
  4. import java.awt.event.*;
  5. import javax.swing.*;
  6. import common.*;
  7.  
  8. public class View implements IView {
  9.  
  10. private JFrame frame;
  11. public JComboBox comboBox;
  12. public JTextArea textArea;
  13.  
  14. @Override
  15. public void initalizeAndDisplayView(final Presenter presenter) {
  16. frame = new JFrame("Nifty Search Application");
  17. frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
  18. JPanel pane = new JPanel();
  19. comboBox = new JComboBox(presenter.getSearchOptionSelectionStrings());
  20. JButton searchButton = new JButton("Search");
  21. textArea = new JTextArea();
  22.  
  23. searchButton.addActionListener(new ActionListener() {
  24. @Override
  25. public void actionPerformed(ActionEvent actionEvent) {
  26. presenter.searchButtonPressed();
  27. }
  28. });
  29.  
  30. pane.add(comboBox);
  31. pane.add(searchButton);
  32. pane.add(textArea);
  33.  
  34. frame.add(pane, BorderLayout.CENTER);
  35. frame.pack();
  36. frame.setVisible(true);
  37. }
  38.  
  39. @Override
  40. public int getSelectedSearchIndex() {
  41. return comboBox.getSelectedIndex();
  42. }
  43.  
  44. @Override
  45. public void setDisplayText(String displayText) {
  46. textArea.setText(displayText);
  47. frame.pack();
  48. }
  49. }

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:

  1. package server;
  2.  
  3. import common.IDatabase;
  4.  
  5. public class Database implements IDatabase {
  6.  
  7. public static String[] DATABASE_OF_VALUES = { //
  8. //
  9. "firstA", "firstB", //
  10. "secondA", "secondB", //
  11. "thirdA", "thirdB", //
  12. };
  13.  
  14. @Override
  15. public String[] getData() {
  16. return Database.DATABASE_OF_VALUES;
  17. }
  18. }

Now it runs successfully.

Success!

The application runs, and produces the expected startup screen:

Application Runs

The drop-down list box looks good:

When searching for "A" records, we get the expected result:

Drop Down MenuSearch Result

When searching for "B" records, we get the expected result:

Search Result B

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...

  1. selectedSearchIndex = 2, as expected
  2. searchCode = null, as expressed in PresenterTest. (The testAll method in that class contains the code 'expect(model.performSearch(null)).andReturn(...);'.)
  3. But when we step into the Model code, where the system tries to evaluate the line
    if (record.endsWith(searchValue)) {
    with the values
    • record = "firstA"
    • searchValue = null
    We get the NullPointerException.

This bug reveals the conflict between the following two tests:

  1. ...
  2. public class PresenterTest extends TestCase {
  3. ...
  4. public void testAll() {
  5. expect(view.getSelectedSearchIndex()).andReturn(
  6. searchOptions.indexOf("- ALL -"));
  7. expect(model.performSearch(<u>null</u>)).andReturn(
  8. Arrays.asList("A value", "B value", "C value"));
  9. view.setDisplayText("[A value, B value, C value]");
  10. finishSetup();
  11.  
  12. presenter.searchButtonPressed();
  13. }
  14. ...
  15. }

and

  1. ...
  2. public class ModelTest extends TestCase {
  3. ...
  4. public void testAll() {
  5. String noSearchRestriction = <u>""</u>;
  6.  
  7. List<String> searchResults = model.performSearch(noSearchRestriction);
  8.  
  9. assertEquals(Arrays.asList("valueA", "valueB"), searchResults);
  10. }
  11. }

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:

 
  1. package integration;
  2.  
  3. import javax.swing.JComboBox;
  4. import org.springframework.test.AbstractDependencyInjectionSpringContextTests;
  5. import client.Presenter;
  6. import client.View;
  7. import common.IDatabase;
  8.  
  9. public class IntegrationTest extends
  10. AbstractDependencyInjectionSpringContextTests {
  11.  
  12. protected View view;
  13. protected IDatabase database;
  14. protected Presenter presenter;
  15.  
  16. public IntegrationTest() throws Exception {
  17. setPopulateProtectedVariables(true);
  18. }
  19.  
  20. @Override
  21. protected String[] getConfigLocations() {
  22. return new String[] { "/context.xml" };
  23. }
  24.  
  25. public void testSearchAOnly() {
  26. presenter.displayViewOnScreen();
  27.  
  28. selectItem(view.comboBox, "A only");
  29. presenter.searchButtonPressed();
  30.  
  31. assertEquals("[firstA, secondA, thirdA]", view.textArea.getText());
  32. }
  33.  
  34. public void testSearchBOnly() {
  35. presenter.displayViewOnScreen();
  36.  
  37. selectItem(view.comboBox, "B only");
  38. presenter.searchButtonPressed();
  39.  
  40. assertEquals("[firstB, secondB, thirdB]", view.textArea.getText());
  41. }
  42.  
  43. private static void selectItem(JComboBox comboBox, String itemValue) {
  44. comboBox.setSelectedItem(itemValue);
  45. assertEquals(itemValue, comboBox.getSelectedItem());
  46. }
  47. }

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:

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:

  1. package integration;
  2.  
  3. import static org.easymock.EasyMock.*;
  4. import java.util.Arrays;
  5. import java.util.List;
  6. import org.springframework.test.AbstractDependencyInjectionSpringContextTests;
  7. import client.Presenter;
  8. import common.IDatabase;
  9. import common.IView;
  10.  
  11. public class IntegrationTest extends
  12. AbstractDependencyInjectionSpringContextTests {
  13.  
  14. protected IView view;
  15. protected IDatabase database;
  16. protected Presenter presenter;
  17. private List<String> searchOptions;
  18.  
  19. public IntegrationTest() throws Exception {
  20. setPopulateProtectedVariables(true);
  21. }
  22.  
  23. @Override
  24. protected String[] getConfigLocations() {
  25. return new String[] { "/main-context.xml",
  26. "/integration/test-context.xml" };
  27. }
  28.  
  29. @Override
  30. protected void onSetUp() throws Exception {
  31. reset(view, database);
  32. view.initalizeAndDisplayView(presenter);
  33. expect(database.getData()).andReturn(
  34. new String[] { "valueA", "valueB" }).anyTimes();
  35.  
  36. searchOptions = Arrays.asList(presenter
  37. .getSearchOptionSelectionStrings());
  38. }
  39.  
  40. private void doFinishSetup() {
  41. replay(view, database);
  42. presenter.displayViewOnScreen();
  43. }
  44.  
  45. @Override
  46. protected void onTearDown() throws Exception {
  47. verify(view, database);
  48. }
  49.  
  50. public void testSearchAOnly() {
  51. expect(view.getSelectedSearchIndex()).andReturn(
  52. searchOptions.indexOf("A only"));
  53. view.setDisplayText("[valueA]");
  54. doFinishSetup();
  55.  
  56. presenter.searchButtonPressed();
  57. }
  58.  
  59. public void testSearchBOnly() {
  60. expect(view.getSelectedSearchIndex()).andReturn(
  61. searchOptions.indexOf("B only"));
  62. view.setDisplayText("[valueB]");
  63. doFinishSetup();
  64.  
  65. presenter.searchButtonPressed();
  66. }
  67.  
  68. public void testSearchAll() {
  69. expect(view.getSelectedSearchIndex()).andReturn(
  70. searchOptions.indexOf("- ALL -"));
  71. view.setDisplayText("[valueA, valueB]");
  72. doFinishSetup();
  73.  
  74. presenter.searchButtonPressed();
  75. }
  76. }

Fixing the Bug

The last test above, testSearchAll illustrates the integration error we are trying to fix, by failing with this error:

  1. java.lang.AssertionError:
  2. Unexpected method call setDisplayText("java.lang.NullPointerException"):
  3. getSelectedSearchIndex(): expected: 1, actual: 1
  4. setDisplayText("[valueA, valueB]"): expected: 1, actual: 0
  5. ...

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:

  1. public void testAll() {
  2. expect(view.getSelectedSearchIndex()).andReturn(
  3. searchOptions.indexOf("- ALL -"));
  4. expect(model.performSearch("")).andReturn(
  5. Arrays.asList("A value", "B value", "C value"));
  6. view.setDisplayText("[A value, B value, C value]");
  7. finishSetup();
  8.  
  9. presenter.searchButtonPressed();
  10. }

Successful Search

So now, at last, the "- ALL -" search works correctly:

Search Result All

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:

  1. We extended Spring's AbstractDependencyInjectionSpringContextTests to create our JUnit test.
  2. We split our Spring configuration file, separating out the beans we wished to mock into a separate file. (additional-context.xml)
  3. 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.)
  4. In the tests, we used EasyMock's reset function to clear the mock objects back to their original state, and then used method calls, and replay and verify functions to specify and verify correct behavior.

Reflection

So what did we learn?

We learned that...

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:

  1. ...
  2. expect(database.getData()).andReturn(new String[] { "valueA", "valueB" }).anyTimes();
  3. ...
  4.  
  5. public void testSearchAll() {
  6. expect(view.getSelectedSearchIndex()).andReturn(
  7. searchOptions.indexOf("- ALL -"));
  8. view.setDisplayText("[valueA, valueB]");
  9. doFinishSetup();
  10.  
  11. presenter.searchButtonPressed();
  12. }

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:

  1. ...
  2. expect(database.getData()).andReturn(new String[] {"valueA", "valueB"}).anyTimes();
  3. ...
  4.  
  5. public void testAll() {
  6. String noSearchRestriction = "";
  7.  
  8. List<String> searchResults = model.performSearch(noSearchRestriction);
  9.  
  10. assertEquals(Arrays.asList("valueA", "valueB"), searchResults);
  11. }

And it is similar to the PresenterTest to search all, which could be written like this:

  1. public void testAll() {
  2. expect(view.getSelectedSearchIndex()).andReturn(searchOptions.indexOf("- ALL -"));
  3. expect(model.performSearch("")).andReturn(Arrays.asList("valueA", "valueB"));
  4. view.setDisplayText("[valueA, valueB]");
  5. finishSetup();
  6.  
  7. presenter.searchButtonPressed();
  8. }

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

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

Glossary

Libraries