Spring Profiles and Reusable Tests, Part 2

In the 1st part of the post, we were able to use Spring profiles to inject a specific implementation of a class to test into some test cases. Now we will try to create a reusable test case by using Spring Dependency Injection and Spring profiles to inject the appropriate expected results when testing a particular implementation.

Firstly we need to get the expected test results into a format that be be injected by Spring DI into a test case.

public interface FormatResults {

  public String getExpectedResult(String testMethodName);
}

public abstract class BaseFormatResults implements FormatResults {

  private Map<String, String>  results;

  public BaseFormatResults()
  {
    results = new HashMap<String, String>();

    setUpResults();
  }

  protected abstract void setUpResults();

  protected void addResult(String testMethodName, String result)
  {
    results.put(testMethodName, result);
  }

  @Override
  public String getExpectedResult(String testMethodName)
  {
    return results.get(testMethodName);
  }
}

public class HelloResults extends BaseFormatResults {

  @Override
  protected void setUpResults()
  {
    addResult("testDave", "Hello Dave");
  }
}

public class GoodByeResults extends BaseFormatResults {

  @Override
  protected void setUpResults()
  {
    addResult("testDave", "Good Bye Dave");
  }
}

For the sample code, a class is created as a wrapper around a map, which stores the expected test results for a test case. The expected result for each test method is keyed by the test method name in the map. Then some methods are included for adding and retrieving these expected results.

I have also made this wrapper class implement an interface and be abstract so that it can be subclassed to add the expected results for a particular test class.

Next we refactor the Spring JavaConfig classes to include these result classes, so that they can be injected into the test case along with the implementation of the class to test.

@Configuration
public class CommonTestConfig {

  @Autowired
  LogTestConfiguration logTestConfiguration;

  @Bean
  public Formatter formatter()
  {
    return logTestConfiguration.formatter();
  }

  @Bean
  public FormatResults results()
  {
    return logTestConfiguration.results();
  }

  @Bean
  public String testData()
  {
    return "Dave";
  }
}

public interface LogTestConfiguration {

  public Formatter formatter();

  public FormatResults results();
}

@Configuration
@Profile("hello")
public class HelloConfig implements LogTestConfiguration {

  @Bean
  public Formatter formatter()
  {
    return new HelloFormatter();
  }

  @Bean
  public FormatResults results()
  {
    return new HelloResults();
  }
}

@Configuration
@Profile("goodbye")
public class GoodByeConfig implements LogTestConfiguration {

  @Bean
  public Formatter formatter()
  {
    return new GoodByeFormatter();
  }

  @Bean
  public FormatResults results()
  {
    return new GoodByeResults();
  }
}

An extra method is added to the ‘LogTestConfiguration’ interface to retrieve a result class. Then the extra @Bean method to the configuration classes for getting the appropriate result class based on the active Spring profile.

Lastly we again refactor the test case to use the updated configuration files.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={CommonTestConfig.class, HelloConfig.class, GoodByeConfig.class})
public class SpringProfileTest {

  @Autowired
  private Formatter formatter;

  @Autowired
  private FormatResults results;

  @Autowired
  private String testData;

  @Test
  public void testDave()
  {
    String result = formatter.format(testData);
    System.out.println("Result = " + result);

    String expected = results.getExpectedResult("testDave");

    assertEquals(expected, result);
  }

  // getters and setters left out for brevity ...
}

Here are the changes from the test case examples in the previous post:

  • there is now only one test case required to test either test class implemenation ‘HelloFormatter’ or ‘GoodByeFormatter’ (previously a separate test case was required for each)
  •  the results for the test is also now injected into the test case via Spring DI
  • all of the Spring configuration files are included in the @ContextConfiguration annotation
  • the @ActiveProfiles annotation has been left out, now we need to specify the active Spring profile to use external to the test case, this can be done by setting the ‘spring.profiles.active’ property before running the test case, and there are various ways to do this.

For the sample code, if you are running the test case using the Eclipse IDE, then this could be set in the Run Configuration for the JUnit test class.

Eclipse JUnit run configuration

If you are using Gradle, then just set the property in the build script in the ‘test’ task.

test {
  systemProperties 'spring.profiles.active': 'hello'
}

The Sample Code

You can download the sample code for this example, SpringProfileTestingExample2.zip from GitHub.

Note that I have kept the sample code very simple for the sake of brevity, and also to more clearly illustrate the point. For instance, you may need to add the @DirtiesContext annotation to the test classes if you don’t want the injected beans to be cached between tests.

Conclusion

Using Spring bean definition profiles, we can make test cases reusable in the particular scenario where we are testing different implementations of a class.

If you only have a few tests to run, then the this setup with the Spring profiles would be overkill.

However for my project, where I had many test cases, it allowed me to reuse them without having to duplicate them for each implementation of a class under test. Of course, it would also allow me to use those same test cases to test any future implementations of the class too.

Spring Profiles and Reusable Tests, Part 1

When writing test cases for an update to an open source project I was working on, I came across the issue of how to make test classes reusable.

I already had dozens of test cases written for testing a particular class. Now I had written another implementation of that class, and I wanted to reuse the test cases I already had, to avoid duplication.

I decided to see if I could use Spring profiles to do this.

Spring Profiles

Spring bean definition profiles allows the wiring of different beans in different environments.

One use of Spring profiles in testing is where different test data or application configurations can be injected into test cases depending on the profile. There are various examples of this:

However my requirements were a bit different.

  • more specifically I had various implementations of a class that logged an object graph and produce some output in differing formats.
  • I had many test cases written that would test a particular implementation of the logging class and then compare the generated logging with some expected output.
  • I wanted to run the same set of tests with the same test data, but testing different implementations of the logging class

So rather than changing the test data, what I wanted to change was the class under test and the expected results using Spring DI. Using profiles would allow me to reuse the same tests and test data.

Of course if there are only a few tests to run, this could also be achieved using parameterized tests. Then for each test you could use JUnit 4 to pass parameters for the test data and expected test results.

Here are some simple code examples, showing one way to use Spring profiles in tests.

Step 1: A Simple Test with Spring

Note: Here I’m also using Spring’s JavaConfig instead of XML for the application contexts, but either would work.

Firstly let’s create some classes to test. Here are 2 classes that do some string formatting, both of which have a common interface:

public interface Formatter {

  public String format(String name);
}

public class HelloFormatter implements Formatter {

  @Override
  public String format(String name) {
    return "Hello " + name;
  }
}

public class GoodByeFormatter implements Formatter {

  @Override
  public String format(String name) {
    return "Good Bye " + name;
  }
}

Now let’s write a simple JUnit test case for to test ‘HelloFormatter’, using Spring to inject the test class and test data.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={SimpleTestConfig.class})
public class SimpleTest {

	@Autowired
	private Formatter formatter;

	@Autowired
	private String testData;

	@Test
	public void testDave() {
		String result = formatter.format(testData);
		System.out.println("Result = " + result);

		String expected = "Hello Dave";

		assertEquals(expected, result);
	}

	// getters and setters left out for brevity ...
}

And here’s the JavaConfig used to wire the beans for the test.

@Configuration
public class SimpleTestConfig {

	@Bean
	public Formatter formatter()
	{
		return new HelloFormatter();
	}

	@Bean
	public String testData()
	{
		return "Dave";
	}
}

Now if we want to test the ‘GoodbyeFormatter’ implementation class as well, we could just write another test case and another Spring JavaConfig class, but this would mean code duplication.

So instead let’s try using Spring profiles to inject the appropriate test class into the test cases.

Step 2 : Using Spring Profiles

Firstly let’s refactor the Spring JavaConfig classes.

@Configuration
public class CommonTestConfig {

	@Autowired
	LogTestConfiguration logTestConfiguration;

	@Bean
	public Formatter formatter()
	{
		return logTestConfiguration.formatter();
	}

	@Bean
	public String testData()
	{
		return "Dave";
	}
}

public interface LogTestConfiguration {

	public Formatter formatter();

}

@Configuration
@Profile("hello")
public class HelloConfig implements LogTestConfiguration {

	@Bean
	public Formatter formatter()
	{
		return new HelloFormatter();
	}
}

@Configuration
@Profile("goodbye")
public class GoodByeConfig implements LogTestConfiguration {

	@Bean
	public Formatter formatter()
	{
		return new GoodByeFormatter();
	}
}

Notice we have split the configuration between 3 classes now:

  • there is one class that configures the beans required for all the test cases
  • there is one configuration file specifically for testing the ‘HelloFormatter’ class, and it has the annotation ‘@Profile(“hello”)’.
  • there is one configuration file specifically for testing the ‘GoodByeFormatter’ class, and it has the annotation ‘@Profile(“goodbye”)’.
  • the 2 configuration files that have profile annotations share a common interface so that they can, in turn, be injected into the common configuration file

This setup is base on the blog, SPRING 3.1 M1: INTRODUCING @PROFILE, which contains a more detailed explanation.

So here are the refactored test cases that use the new configuration files, one test class to test each formatter implementation class.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={CommonTestConfig.class, HelloConfig.class})
@ActiveProfiles("hello")
public class SpringProfileHelloTest {

	@Autowired
	private Formatter formatter;

	@Autowired
	private String testData;

	@Test
	public void testDave() {
		String result = formatter.format(testData);
		System.out.println("Result = " + result);

		String expected = "Hello Dave";

		assertEquals(expected, result);
	}

	// getters and setters left out for brevity ...
}

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={CommonTestConfig.class, GoodByeConfig.class})
@ActiveProfiles("goodbye")
public class SpringProfileGoodbyeTest {

	@Autowired
	private Formatter formatter;

	@Autowired
	private String testData;

	@Test
	public void testDave() {
		String result = formatter.format(testData);
		System.out.println("Result = " + result);

		String expected = "Good Bye Dave";

		assertEquals(expected, result);
	}

	// getters and setters left out for brevity ...
}

Note that each test case class has the @ActiveProfiles annotation, so that only the appropriate test class is injected for testing. Also we have added the required configuration files to the @ContextConfiguration annotation.

The Sample Code

You can download the sample code for this example, SpringProfileTestingExample1.zip from GitHub.

The requirements to run the sample code are:

Next

We have achieved half of what we set out to do, by using Spring profiles to determine which implementation of a test class is used for each test case.

In the next part of this post, we will make further improvements so that we have one reusable test case able to test both test class implementations by injecting the expected test results as well.