Tuesday, January 10, 2017

Unit testing Spring caching with grails

Grails unit tests do no autowire by default (certainly in version 2.2.3) , so to enable caching in a unit test we had to jump through a few hoops.

The easiest thing in the end was to manually create an xml to load the bean in question. (Once we created the bean in the xml, then the cachable annotations were recognized)
This worked in terms of loading the bean with the caching functionality built in, but then we began to run into class cast exceptions, because of the way that spring implements the caching (using proxys). See http://spring.io/blog/2012/05/23/transactions-caching-and-aop-understanding-proxy-usage-in-spring

The easiest solution we found to this, was to create an interface for the service in question. Then the proxying was able to cast the dynamically generated proxyClass to the interface.

Test xml (in test/unit)

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

   

   
   
          class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="ehcache"/>

   
   
          class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="TestEhCache.xml"/>




EhCache.xml (in grails-app/conf)


        xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
        xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">


   
                  maxElementsInMemory='100'
                  overflowToDisk='false' />

   
           maxElementsInMemory="100"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           memoryStoreEvictionPolicy="LFU"/>




Interface
import org.springframework.cache.annotation.Cacheable
interface MyServiceIF  {

    // calling stored procedure to determine the as_of_date
    @Cacheable("priorToDate")
    public Date priorToDate(String yyyymmdd);

}

Class
class MyService implements MyServiceIF  {

    static transactional = false

    public Date priorToDate(String yyyymmdd) {
        return evaluate(yyyymmdd, -1);
    }
}

Spock Test

Note also that if you are declaring a method cacheable, with multiple parameters, then you may want to define a keyGenerator, (or ignore the params)
 public void validateCache() {
        given:
        CacheManager cacheManager = ctx.getBean("cacheManager")
        String result;

        when:
        Cache dateCache = cacheManager.getCache(testName);
        String result1FromCache  = dateCache.get(dateToTest);   // Verify that the cache is empty
        Object resultFromSds
        Object result2FromSds
        Object result2FromCache
        if(testName =="futureBusinessDate" || testName == "pastBusinessDate"){
            resultFromSds = dateToString(daoService."$testName"(dateToTest,1 ))
            Object key = new DefaultKeyGenerator().generate(daoService, DalSdsDateIF.class.getMethod(testName, String.class, int.class), dateToTest, 1)   //compund params, so must generate key
            result2FromCache = dateToString(dateCache.get(key).get());
            result2FromSds = dateToString(daoService."$testName"(dateToTest,1 ))
        } else  {
            resultFromSds = dateToString(daoService."$testName"(dateToTest) )
            result2FromCache = dateToString(dateCache.get(dateToTest).get());
            result2FromSds = dateToString(daoService."$testName"(dateToTest) ) // expect this come from cache, so will not call log again
        }

        then:
        dateCache!=null
        result1FromCache==null    //verify cache is empty
        resultFromSds==expectedResult
        result2FromCache==expectedResult
        result2FromSds==expectedResult
        count ==expectedCallsToLog    // count number of calls  to log.info.. Expect one per call, except for isBusinessDate

        where:
        testName            |  dateToTest   | expectedResult | expectedCallsToLog
        "priorToDate"       | "2016-09-06"  | "2016-09-02"   | 1
        "nextToDate"        | "2016-09-02"  | "2016-09-06"   | 1
        "futureBusinessDate"| "2016-09-02"  | "2016-09-06"   | 1
        "pastBusinessDate"  | "2016-09-06"  | "2016-09-02"   | 1

    }

e.g. in the test
Object key = new DefaultKeyGenerator().generate(daoService, DalSdsDateIF.class.getMethod(testName, String.class, int.class), dateToTest, 1)   //compund params, so must generate key


If you have parameters in the method call that you don't want influsencing the cahce (e.g. ignroe them you can do this, or this)
e.g. to ignore params you can do this

 @Cacheable(value="myCache", key="#root.methodName")// Force key name to be fixed no matter what params passed in
 public Map getValues(List warnings){