1

The issue I am facing is that two objects returned from spring cacheable method with a same key fail assertSame test. Why are these objects not sharing one same storage area?

Details: I am using redis cache mechanism to implement caching in a spring boot REST api. The caching works correctly in the way that it first retrieve the data from externally provided source (JPS repository accessing a database) and then subsequent calls for the same cache key returns data from cache. However, I am not able to mimic this behavior completely in the JUnit test cases. My assertEquals or assertSame fail on 2 objects returned from the cache.

my code base looks as below: mvn dependencies:

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.7.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

Spring application config:

@SpringBootApplication
@EnableCaching
public class Application {
@Value("${redis.host}")
private String redisHost;

public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
}

@Bean
JedisConnectionFactory jedisConnectionFactory() {
    JedisConnectionFactory jedisConFactory = new JedisConnectionFactory();
    jedisConFactory.setHostName(redisHost);
    jedisConFactory.setPort(6379);
    return jedisConFactory;
}

@Bean
public RedisTemplate<String, Object> redisTemplate() {
    RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
    template.setConnectionFactory(jedisConnectionFactory());

    return template;
}

@Bean
CacheManager cacheManager() {
    return new RedisCacheManager(redisTemplate());
}

Service Class:

@Service
public class CIDomainService {
private RedisTemplate<String, Object> redisTemplate;
private CIDomainDAO ciDomainDAO;

@Autowired
public CIDomainService(CIDomainDAO ciDomainDAO, RedisTemplate<String, Object> redisTemplate) {
    this.ciDomainDAO = ciDomainDAO;
    this.redisTemplate = redisTemplate;
}

@Cacheable(value = "ciDomain", key = "#id")
public CIDomain getCIDomain(int id) {
    CIDomain ciDomain = new CIDomain();
    ciDomain.setId(id);
    ciDomain.setName("SomeName");
    return ciDomain;
}


public void clearAllCache() {
    redisTemplate.delete("listCIDomains");
    redisTemplate.delete("ciDomain");
}

}

ciDomainDAO in the service above is just a JPS repository interface using the findAll() method to retrieve data from external database or in-memory database. My Test class:

@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles("local")
@SpringBootTest
public class CIDomainServiceIntegrationTest {

@Autowired
CIDomainService ciDomainServiceSpy;

@Before
public void setUp(){
   ciDomainServiceSpy.clearAllCache();
}

@Test
public void listCIDomains_ShouldRetrieveCIDomainsWithCachingPluggedIn() {
    CIDomain domain1 = ciDomainServiceSpy.getCIDomain(1);
    CIDomain domain2 = ciDomainServiceSpy.getCIDomain(2);
    CIDomain domain3 = ciDomainServiceSpy.getCIDomain(1);
    assertSame(domain1, domain3);   //fails
}

My Domain Class:

@Entity
@Table(name = "CI_DOMAIN")
public class CIDomain implements Serializable{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;

@Column(name = "name")
private String name;

public int getId() {
    return id;
}

public void setId(int id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

based on this post I understand that object is retrieved from the repository for the very first call and then later call will fetch this object from cache provided same "key" is provided. I am doing the same thing in my test case above but assertSame is failing. Spring cacheable must be caching object in memory which is fetched for a given request. Why would it send different objects everytime for the same requested key.

I have tried to have an alternative solution where I used spy on the service class and verify method calls based on a same key request. However, I encountered different issues in doing that. Creating a spy on the service class does not even use caching mechanism and it does call service getCIDomain method even if same key is provided. I followed this, this, this, this, this and lots of other posts for further analysis but could not get it working either through assertSame of spy.

Any help would really be appreciated.

Vishal
  • 1,963
  • 2
  • 20
  • 23
  • You are checking if they are the same (`assertSame`(, they aren't. They should be equal (`assertEqual`). Being equal and the same are different things. For the latter to pass you need an `equals` and `hashCode` method in your domain object. – M. Deinum Jun 21 '17 at 13:13
  • @M.Deinum Thanks for your response. I can reason your comment based on the fact that serialized and de-serialized a "same" (equals pass) java object has two different references or ID and hence object1 == object2 will be false. However, using equals instead of assertSame is kind of questionable for me due to one reason I think that "I need to use assertSame, otherwise the code depends on .equals(Object), which hasn't been shown. It may have been overridden to compare all properties and then this test would always pass, even if nothing was cached." – Vishal Jun 21 '17 at 14:44
  • same isn't the equals. assertSame does `domain1 == domain3` this will not be the case as it is a different object instance. You should really use `equals.` and not something else. As stated, by yourself, the caching (due to the use of Redis) is serializing and serializing objects and thus they will never be the same. Even if you would use java serialization the objects would never be the same. Trust me you really have to check for object equality and not if they are the same (as they never will be). – M. Deinum Jun 22 '17 at 05:56
  • @M.Deinum Yes that makes sense but as stated above, the only concern I have with using equals is that how would I make sure whether caching is working on not. Let say caching is not working - in that case, the method will fetch data from the original repository and my equals test will still pass because I am retrieving data based on a same ID. So "domain1" and "domain2" both will have same ID and NAME and hence equal will pass even if caching did not really take place. Thats the concern I have. – Vishal Jun 22 '17 at 14:22
  • Your test is wrong I would say. You should test multiple calls and only for the first the actual method should be called, not for subsequent cached results. That is what you should be testing NOT the result of the method. – M. Deinum Jun 22 '17 at 17:54
  • @M.Deinum Please review last paragraph of my original unedited question. – Vishal Jun 22 '17 at 21:03
  • Then your setup is wrong. Just create a mock, and let it call the actual method and that should happen twice instead of 3 times. But why do you even want to test this? You are basically testing a framework here which already has lots of tests in place for this. – M. Deinum Jun 23 '17 at 05:57

1 Answers1

0

I had got this issue resolved and was able to design the test case as it should be for verifying spring cacheable mechanism.

Just providing my analysis and resolution below to help someone out there facing this same issue.

I mentioned in my comments and original questions above that assertSame would not work due to how serialization works and assertEquals though was working but it was kind of not satisfying my test requirement.

The conclusion I made (based on comments) that I should actually test number of method calls and not the result. I tried to mock the CIDomainDAO repository dao as in my question but I faced with couple issues. Creating mocked object of CIDomainDAO and passing it in the CIDomainService constructor was not triggering spring cache and my test was failing. If I do not mock CIDomainDAO and tried spying on CIDomainService to check no of method calls and ran my test then I was ending up getting

org.mockito.exceptions.misusing.UnfinishedVerificationException: Missing 
method call for verify(mock).

This was obvious as mocking does not seem to work on final methods that CIDomainDAO might have had in its spring generated JPARepository implementation. This post helped me understand this behavior of mockito.

Concluding that I need to mock CIDomainDAO somehow, I ended up injecting mocked version of CIDomainDAO respository in CIDomainService class. I had to define a CIDomainDAO setter in CIDomainService class specially for this purpose. After that I tried no of method calls and it worked as expected i.e., service called two times but CIDomainDAO called once as the data was returned from the cache in the second call.

Provided below the modified classes from my original question above.

The service class:

@Service
public class CIDomainService {
    private RedisTemplate<String, Object> redisTemplate;
    private CIDomainDAO ciDomainDAO;

    @Autowired
    public CIDomainService(CIDomainDAO ciDomainDAO, RedisTemplate<String, 
    Object> redisTemplate) {
        this.ciDomainDAO = ciDomainDAO;
        this.redisTemplate = redisTemplate;
    }

    @Cacheable(value = "ciDomain", key = "#id")
    public CIDomain getCIDomain(int id) {
        CIDomain ciDomain = new CIDomain();
        ciDomain.setId(id);
        ciDomain.setName("SomeName");
        return ciDomain;
    }


    public void clearAllCache() {
        redisTemplate.delete("listCIDomains");
        redisTemplate.delete("ciDomain");
    }

    public void setCIDomainDAO(CIDomainDAO ciDomainDAO ) {
        this.ciDomainDAO = ciDomainDAO;
    }
}

And this is the updated test case:

@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles("local")
@SpringBootTest
public class CIDomainServiceIntegrationTest {

    @Autowired
    @InjectMocks
    CIDomainService ciDomainService;

    @Mock
    CIDomainDAO ciDomainDAO;

    @Before
    public void setUp() {
        Mockito.reset(ciDomainDAO);
        ciDomainService.clearAllCache();
    }


    @Test
    public void listCIDomains_ShouldNotAttemptToCallRepositoryWhenCachingEnabledAfterFirstCallOfRetrievingCIDomains() {
        List<CIDomain> domains1 = ciDomainService.listCIDomains();
        List<CIDomain> domains2 = ciDomainService.listCIDomains();
        Mockito.verify(ciDomainDAO, Mockito.times(1)).findAll();
    }

    @Test
    public void listCIDomains_ShouldAttemptToCallRepositoryWhenCachingIsClearedAfterFirstCallOfRetrievingCIDomains() {
    List<CIDomain> domains1 = ciDomainService.listCIDomains();
        ciDomainService.clearAllCache();
        List<CIDomain> domains2 = ciDomainService.listCIDomains();
        Mockito.verify(ciDomainDAO, Mockito.times(2)).findAll();
    }

    @After
    public void postSetUp() {
        Mockito.validateMockitoUsage();
        ciDomainService.clearAllCache();
        Mockito.reset(ciDomainDAO);
      }
    }
Vishal
  • 1,963
  • 2
  • 20
  • 23