【问题标题】:Spring Casheable returned cached objects fail equality checkSpring Cacheable 返回缓存对象未通过相等性检查
【发布时间】:2017-11-24 09:09:03
【问题描述】:

我面临的问题是从 spring 可缓存方法返回的具有相同键的两个对象未通过 assertSame 测试。为什么这些对象不共享同一个存储区域?

详情: 我正在使用redis缓存机制在spring boot REST api中实现缓存。 缓存正常工作的方式是它首先从外部提供的源(访问数据库的 JPS 存储库)检索数据,然后对相同缓存键的后续调用从缓存中返回数据。但是,我无法在 JUnit 测试用例中完全模仿这种行为。我的 assertEquals 或 assertSame 在从缓存返回的 2 个对象上失败。

我的代码库如下所示: mvn 依赖:

<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 应用配置:

@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
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 只是一个JPS 存储库接口,它使用findAll() 方法从外部数据库或内存数据库中检索数据。我的测试课:

@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
}

我的领域类:

@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;
}

基于this post 我知道第一次调用时从存储库中检索对象,然后如果提供了相同的“密钥”,稍后调用将从缓存中获取该对象。我在上面的测试用例中做同样的事情,但 assertSame 失败了。 Spring cacheable 必须是在内存中为给定请求获取的缓存对象。为什么每次都为相同的请求密钥发送不同的对象。

我尝试了另一种解决方案,我在服务类上使用了 spy 并根据相同的密钥请求验证方法调用。但是,我在这样做时遇到了不同的问题。在服务类上创建间谍甚至不使用缓存机制,即使提供了相同的密钥,它也会调用服务 getCIDomain 方法。我关注了thisthisthisthisthis 和许多其他帖子以进行进一步分析,但无法通过间谍的 assertSame 使其工作。

任何帮助将不胜感激。

【问题讨论】:

  • 您正在检查它们是否相同(assertSame(,它们不是。它们应该相等(assertEqual)。相等和相同是不同的事情。对于后者通过你的域对象中需要一个equalshashCode 方法。
  • @M.Deinum 感谢您的回复。我可以根据序列化和反序列化“相同”(等于通过)java 对象具有两个不同的引用或 ID 的事实来推断您的评论,因此 object1 == object2 将是错误的。但是,使用 equals 代替 assertSame 对我来说有点问题,原因之一是我认为“我需要使用 assertSame,否则代码依赖于 .equals(Object),它尚未显示。它可能已被覆盖比较所有属性,然后此测试将始终通过,即使没有缓存任何内容。"
  • 相同不等于。 assertSame 确实domain1 == domain3 不会出现这种情况,因为它是一个不同的对象实例。你真的应该使用equals. 而不是别的东西。如前所述,缓存(由于使用 Redis)正在序列化和序列化对象,因此它们永远不会相同。即使您使用 java 序列化,对象也永远不会相同。相信我,您确实必须检查对象是否相等,而不是检查它们是否相同(因为它们永远不会相同)。
  • @M.Deinum 是的,这是有道理的,但如上所述,我对使用 equals 唯一关心的是,我如何确保缓存是否正常工作。假设缓存不起作用 - 在这种情况下,该方法将从原始存储库中获取数据,并且我的 equals 测试仍将通过,因为我正在根据相同的 ID 检索数据。因此,“domain1”和“domain2”都将具有相同的 ID 和 NAME,因此即使缓存没有真正发生,也会通过相等的。这就是我的担忧。
  • 我会说你的测试是错误的。您应该测试多个调用,并且只有第一次调用实际方法,而不是随后的缓存结果。那是你应该测试的,而不是方法的结果。

标签: spring caching redis


【解决方案1】:

我已经解决了这个问题,并且能够设计测试用例,因为它应该用于验证 Spring 可缓存机制。

只需在下面提供我的分析和解决方案,以帮助面临同样问题的人。

我在上面的 cmets 和原始问题中提到 assertSame 由于序列化的工作原理而无法工作,而 assertEquals 虽然工作但有点不满足我的测试要求。

我得出的结论(基于 cmets)我应该实际测试方法调用的数量而不是结果。我试图在我的问题中模拟 CDomainDAO 存储库 dao,但我遇到了几个问题。创建 CIDomainDAO 的模拟对象并将其传递给 CIDomainService 构造函数没有触发弹簧缓存,我的测试失败了。如果我不模拟 CIDomainDAO 并尝试监视 CIDomainService 以检查没有方法调用并运行我的测试,那么我最终会得到 ​​p>

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

这很明显,因为模拟似乎不适用于 CIDomainDAO 在其 spring 生成的 JPARepository 实现中可能具有的最终方法。 This 的帖子帮助我理解了 mockito 的这种行为。

得出我需要以某种方式模拟 CIDomainDAO 的结论,我最终在 CIDomainService 类中注入了模拟版本的 CIDomainDAO 存储库。为此,我必须在 CIDomainService 类中专门定义一个 CIDomainDAO 设置器。之后我没有尝试任何方法调用,它按预期工作,即服务调用了两次,但 CDomainDAO 调用了一次,因为数据是在第二次调用中从缓存中返回的。

在我上面原始问题的修改类下方提供。

服务类:

@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;
    }
}

这是更新后的测试用例:

@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);
      }
    }

【讨论】:

    猜你喜欢
    • 2018-03-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-07
    • 2014-03-16
    • 2012-06-05
    • 1970-01-01
    • 2015-07-19
    相关资源
    最近更新 更多