Testing a cacheable method with Mockito

The Spring framework provides a simple way to cache the value returned by a method.
Basically, you annotate a method with @Cacheable and next time you call it with the same arguments, you get the cached value avoiding re-executing the method’s code.
In the example we’re going to use, the Spring cache infrastructure will be backed by Ehcache. These are the basic Maven dependencies we’ll need:

	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-context</artifactId>
		<version>3.1.2.RELEASE</version>
	</dependency>
	<dependency>
		<groupId>net.sf.ehcache</groupId>
		<artifactId>ehcache-core</artifactId>
		<version>2.5.0</version>
		<scope>runtime</scope>
	</dependency>
	<dependency>
		<groupId>cglib</groupId>
		<artifactId>cglib</artifactId>
		<version>2.2.2</version>
		<scope>runtime</scope>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-test</artifactId>
		<version>3.1.2.RELEASE</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.11</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.mockito</groupId>
		<artifactId>mockito-all</artifactId>
		<version>1.9.5</version>
		<scope>test</scope>
	</dependency>

The method we’re going to cache is a service method that returns some (possibly expensive) database record count:

import org.springframework.cache.annotation.Cacheable;

public class CacheableService {

	private EntityDao entityDao;

	public int someEntityCount(final CriteriaDto criteria) {
		return entityDao.count(criteria);
	}
	[...]

The CriteriaDto class is as follows:

public class CriteriaDto {

	private String criterion1;
	private String criterion2;
	private String otherProperty;
	
	[...]

We want that method to return the cached value whenever it’s repeatedly invoked with the same criteria. To complicate matters further, let’s suppose that only the fields criterion1 and criterion2 affect the returned record count.
Those requirements can be met by preceding the method with the following annotation:

	@Cacheable(value = "entityCount", key = "{ #criteria.criterion1, #criteria.criterion2 }")
	public int someEntityCount(final CriteriaDto criteria) {
	[...]

As you can see, the relevant criteria for the cache can be specified via a SpEL expression in the key argument. The other argument, value is the name of the cache that will be used.

Assuming you’re using the venerable XML Spring configuration, you can set up the cache service with the following XML fragment in your application context:

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:jee="http://www.springframework.org/schema/jee"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:cache="http://www.springframework.org/schema/cache"
	xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd   http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd   http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd   http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
	http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">

	 [...]
	 
	<cache:annotation-driven cache-manager="countCacheManager"/>

	<bean id="countCacheManager"
		  class="org.springframework.cache.ehcache.EhCacheCacheManager">
		  <property name="cacheManager" ref="ehcache"></property>
	</bean>

	<bean id="ehcache"
		  class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
		  <property name="configLocation" value="META-INF/count-ehcache.xml"></property>
	</bean>
	 [...]

Lastly, we need to provide the referenced Ehcache configuration file (count-ehcache.xml in this case):

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
	<cache 
	       name="entityCount"
	       maxElementsInMemory="10"
	       timeToLiveSeconds="300"
	       logging="true"
	       statistics="true"
	        />    
</ehcache>

The important configuration item here is the cache’s name attribute, which must match the value specified in the @Cacheable annotation.
Now it’s time to test the correct behaviour of our cached method.
The cache will work as expected if the actual method’s code is executed only the first time the method is called with equivalent criteria.
First of all, let’s prepare a test with two different but equivalent CriteriaDTO objects.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/META-INF/applicationContext.xml")
public class CacheableServiceTest {
	
	@Autowired
	private CacheableService serviceUnderTest;
	
	@Test
	public void testSomeEntityCountCalledTwice_shouldCallDaoMethodOnce() {
		
		// Local fixture set-up
		CriteriaDto criteria1 = new CriteriaDto();
		criteria1.setCriterion1("c1");
		criteria1.setCriterion2("c2");
		criteria1.setOtherProperty("c3a"); // Should be ignored by Cacheable annotation
		
		CriteriaDto criteria2 = new CriteriaDto();
		criteria2.setCriterion1("c1");
		criteria2.setCriterion2("c2");
		criteria2.setOtherProperty("c3b"); // Should be ignored by Cacheable annotation
	[...]

The class under test, CacheableService has a collaborating class: EntityDao. We’re going to substitute it with a mock.

	@Test
	public void testSomeEntityCountCalledTwice_shouldCallDaoMethodOnce() {
		
		// Local fixture set-up
		CriteriaDto criteria1 = new CriteriaDto();
		criteria1.setCriterion1("c1");
		criteria1.setCriterion2("c2");
		criteria1.setOtherProperty("c3a"); // Should be ignored by Cacheable annotation
		
		CriteriaDto criteria2 = new CriteriaDto();
		criteria2.setCriterion1("c1");
		criteria2.setCriterion2("c2");
		criteria2.setOtherProperty("c3b"); // Should be ignored by Cacheable annotation
		
		// Mock installation
		EntityDao mockEntityDao = Mockito.mock(EntityDao.class);
		serviceUnderTest.setEntityDao(mockEntityDao);
		[...]

Lastly, we’re going to call the method under test (someEntityCount) with both criteria objects. If everything works as expected, the second call will return the value cached in the first invocation, avoiding the call to the potentially expensive DAO method.
Since the DAO object used in the test is actually a mock object, we can use Mockito.verify to check its use. More specifically, we’ll use the optional VerificationMode parameter to make sure that the method has only been called once.
Here’s the full test method:

	@Test
	public void testSomeEntityCountCalledTwice_shouldCallDaoMethodOnce() {
		
		// Local fixture set-up
		CriteriaDto criteria1 = new CriteriaDto();
		criteria1.setCriterion1("c1");
		criteria1.setCriterion2("c2");
		criteria1.setOtherProperty("c3a"); // Should be ignored by Cacheable annotation
		
		CriteriaDto criteria2 = new CriteriaDto();
		criteria2.setCriterion1("c1");
		criteria2.setCriterion2("c2");
		criteria2.setOtherProperty("c3b"); // Should be ignored by Cacheable annotation
		
		// Mock installation
		EntityDao mockEntityDao = Mockito.mock(EntityDao.class);
		serviceUnderTest.setEntityDao(mockEntityDao);
		
		// First call with criteria1
		serviceUnderTest.someEntityCount(criteria1);
		
		// Second call with criteria2
		serviceUnderTest.someEntityCount(criteria2);
		
		// Verification
		Mockito.verify(mockEntityDao, Mockito.times(1)).count(Mockito.any(CriteriaDto.class));
	}

As you can see, we’ve used the method times to specify the number of times we expect the method to have been called during the test and any to tell Mockito that we don’t care about the actual parameters passed in the method invocations.

This is an easy way to test the behaviour of a cached method. It could be further refined in several ways: verifying the call with the right parameters, adding a method to test that the cached value is not returned when the criteria objects differ, etc.
I hope you find this article useful and, as usual, I’ll be glad to read your comments.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s