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