jUnit @Rule and Spring Caches
If you have worked a while with jUnit you may have seen jUnit Rules. Simply put, a field in a test class annotated with @Rule is a class that lets you execute some code that runs before and/or after your unit test, similar to the @Before and @After annotations. Consequently, they solve the same problem, However, the code exercised by the @Before
and @After
annotations is usually tied to the specific test class whereas the logic implemented in a @Rule
tends to be of more generic nature. For example, jUnit provides the TemporaryFolder that creates a temporary folder that can be used during a test and deletes it and any files and folders recursively that were created after the test. Another example is the ExpectedException that is useful when verifying exceptions.
Spring Integration Tests
As stated in the Testing chapter in the Spring Reference Docs:
Once the TestContext framework loads an
ApplicationContext
(orWebApplicationContext
) for a test, that context will be cached and reused for all subsequent tests that declare the same unique context configuration within the same test suite.
Among other things, this means that if your application contains a cache managed by Spring (for example if you use the @Cachable annotation), there is a possibility that there are residual objects from previous tests in the cache when a new test begins. As a consequence, you may experience that the tests will behave different during different executions, for example if a single test is executed compared to when it is executed as part of the entire test suite. To prevent this you can either create a specific @Before
method that clears the cache that can be copied between test files, create a common AbstractTest
class with the eviction logic that can be inherited to all relevant test classes, create a separate TestSupport
class that the test classes can delegate to, or create a custom test rule.
CacheInvalidationRule
@Service
public class CacheInvalidationRule extends ExternalResource {
private final CacheManager cacheManager;
@Autowired
public CacheInvalidationRule(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
protected void before() {
cacheManager.getCacheNames().stream(). // gets the name of all caches as a stream
map(cacheManager::getCache). // map the cache names to a stream of Cache:s
forEach(Cache::clear); // and clear() them
}
}
Alternatively, if you are not yet able to use Java 8 in production:
@Override
protected void before() {
Collection cacheNames = cacheManager.getCacheNames();
for (String name : cacheNames) {
Cache cache = cacheManager.getCache(name);
cache.clear();
}
}
The ExternalResource is an abstract class provided by jUnit which implements the TestResource interface. Moreover, it provides an before()
method and an after()
method that can be overridden. Here, we chose to clear all caches provided by the cache manager.
Usage
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TestApplicationContext.class)
public class ExampleTest {
@Rule
@Autowired
public CacheInvalidationRule cacheInvalidationRule;
@Test
public void testThatDependOnClearCaches() {
// test some application logic
}
}
In order for this to work, the TestApplicationContext
can either @ComponentScan
the package in which the CacheInvalidationRule
is located (since it is annotated with the @Service
annotation) or declare it as a bean, e.g.
@Configuration
@Import(ApplicationContextThatHasCacheManagerDeclaration.class)
public class TestApplicationContext {
@Autowired
private CacheManager cacheManager;
@Bean
CacheInvalidationRule cacheInvalidationRule() {
return new CacheInvalidationRule(cacheManager);
}
}
In either way, the CacheInvalidationRule
is available in the TestApplicationContext
and can thus be autowired in the ExampleTest
. Do not forget the @Rule
annotation required by jUnit.
Changing Cache Implementations
Depending on your application requirements, you may choose different cache implementations. If you use simple caches like EhCache or Guava you can probably use the cache configuration like it is. On the other hand, it may not be desirable to have tests that depend on external infrastructure such as a GemFire cluster. One way to avoid this is to add an in memory cache backed by Java’s standard ConcurrentHashMap
to your test configuration:
@Configuration
@EnableCaching
public class InMemoryCacheTestApplicationContext {
@Bean
@Primary
CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
Set caches = new HashSet<>();
Cache cache = new ConcurrentMapCache("cache");
caches.add(cache);
// potentially create and add more caches
cacheManager.setCaches(caches);
return cacheManager;
}
@Bean
CacheInvalidationRule cacheInvalidationRule() {
return new CacheInvalidationRule(cacheManager());
}
}
In the example above, only a single cache was added to the cache manager, but you can easily imagine how more caches can be added. The inclusion of the @Primary
annotation brings about that it is this cache manager that shall be used by Spring during the test and not the default one that may be included by some other (production) application context.
Caveat
It is only possible to use the cache invalidation rule if you have access to the cache manager that lives in your application context from the test class. For Spring integration tests (i.e. those that use the SpringJUnit4ClassRunner
) this is not a problem. However, if you create a webapp that is tested by firing up an embedded Tomcat or embedded Jetty, which starts Spring internally, then the application context and its cache manager is not available to the test class, at least not out of the box. In contrast, Spring Boot can use its embedded servlet container and still have access to its application context, and thus autowire any of its beans, see my previous blog post Integration Testing a Spring Boot Application.
References
- jUnit Rules
- jUnit @Rule
- jUnit External Resource
- Spring ConcurrentHashMap cache