【问题标题】:How can I assert query while ignoring difference in time produced by LocalDateTime.now()?如何在忽略 LocalDateTime.now() 产生的时间差异的情况下断言查询?
【发布时间】:2017-03-09 13:42:26
【问题描述】:

我在断言 LocalDateTime.now() 方法产生的时间时遇到问题,我想断言整个查询而忽略它产生的时间。 代码如下:

 @Test
public void getLastDrawResult_lotteryIdPresent() {
    Optional<Long> lotteryId = Optional.of(3L);
    Optional<String> name = Optional.empty();
    Optional<String> byName = Optional.empty();
    Optional<String> byDate = Optional.empty();
    Optional<Boolean> jackpotOnly = Optional.empty();

    String query = QUERY_MAIN_BLOCK.replace("/*statement0*/", LocalDateTime.now().plusMinutes(buyingLock).toString()) + CONDITION_SEARCH_BY_LAST_DRAW_RESULT;
    String updatedQuery = query.replace("/*statement1*/", " WHERE d.lottery_info_id = 3") + " AND d.lottery_info_id = 3";

    DrawResultDto drawResultDto = DrawResultDto.builder().id(3L).build();
    List<DrawResultDto> expectedDrawResultDto = singletonList(drawResultDto);

    when(jdbcTemplate.query(updatedQuery, drawResultDtoQueryBuilder.queryMapper)).thenReturn(expectedDrawResultDto);
    List<DrawResultDto> actualDrawResultDto = drawResultDtoQueryBuilder.getLastDrawResult(lotteryId, name, byName, byDate, jackpotOnly);

    verify(jdbcTemplate).query(updatedQuery, drawResultDtoQueryBuilder.queryMapper);
    assertEquals(expectedDrawResultDto, actualDrawResultDto);

这是比较失败的详细信息,因此您可以了解我需要忽略的内容:Comparison Failure Window Screenshot.

这是我正在测试的方法:

public List<DrawResultDto> getLastDrawResult(Optional<Long> lotteryId, Optional<String> name, Optional<String> byName, Optional<String> byDate, Optional<Boolean> jackpotOnly) {
    String query = QUERY_MAIN_BLOCK.replace("/*statement0*/", LocalDateTime.now().plusMinutes(buyingLock).toString()) +
            CONDITION_SEARCH_BY_LAST_DRAW_RESULT;
    StringBuilder builder = new StringBuilder();
    if (lotteryId.isPresent()) {
        builder.append(query.replace("/*statement1*/", " WHERE d.lottery_info_id = " + lotteryId.get()))
                .append(String.format(" AND d.lottery_info_id = %d", lotteryId.get()));
    } else {
        builder.append(query);
        name.ifPresent(nameValue -> builder.append(" AND lower(li.name) LIKE '%").append(SqlUtils.escapeLike(nameValue).toLowerCase()).append("%' "));
        jackpotOnly.ifPresent(jackpotOnlyValue -> {
            if (jackpotOnlyValue) {
                builder.append(" AND ").append(CONDITION_SEARCH_BY_JACKPOT_WIN);
            }
        });
        byName.ifPresent(s -> builder.append(" ORDER BY li.name ").append(SqlUtils.escapeLike(s)));
        byDate.ifPresent(s -> builder.append(" ORDER BY nfd.nearestDrawDate ").append(SqlUtils.escapeLike(s)));
        if (!byName.isPresent() && !byDate.isPresent()){
            builder.append(" ORDER BY li.name desc ");
        }
    }
    return jdbcTemplate.query(builder.toString(), queryMapper);
}

我试图从 Mockito 做matches(),没有运气。我似乎无法让正则表达式工作。就像匹配所有内容,但以下正则表达式:(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3})。其中选择了我需要忽略的部分。

【问题讨论】:

  • 最好不要对我正在测试的类/方法进行任何更改。

标签: java unit-testing junit mocking mockito


【解决方案1】:

这里的最佳实践包括您注入一个Clock,这将使您能够通过覆盖now 方法访问您需要的任何对象(包括LocalDateTime)。

public List<DrawResultDto> getLastDrawResult(
    Optional<Long> lotteryId,
    Optional<String> name,
    Optional<String> byName,
    Optional<String> byDate,
    Optional<Boolean> jackpotOnly,
    Clock clock) {
  LocalDateTime localDateTime = LocalDateTime.now(clock);
  /* ... */
}

Ash's answer 一样,您可以使用当前签名创建一个新 SystemClock 的方法重载,然后测试接受时钟的方法。

/** Your original method signature. No changes to any calling code. */
public List<DrawResultDto> getLastDrawResult(
    Optional<Long> lotteryId,
    Optional<String> name,
    Optional<String> byName,
    Optional<String> byDate,
    Optional<Boolean> jackpotOnly) {
  return getLastDrawResult(lotteryId, name, byName, byDate, jackpotOnly,
      Clock.systemDefaultZone());
}

/** Your original method implementation. Test this one. */
public List<DrawResultDto> getLastDrawResult(
    Optional<Long> lotteryId,
    Optional<String> name,
    Optional<String> byName,
    Optional<String> byDate,
    Optional<Boolean> jackpotOnly,
    Clock clock) {
  LocalDateTime localDateTime = LocalDateTime.now(clock);
  /* ... */
}

这样,在您的测试中,您可以传入来自Clock.fixed 的值。

另外,您应该强烈考虑将此方法切换为参数化查询,JdbcTemplate supports natively。你甚至可以support named parameters using related classes

【讨论】:

  • 感谢您更详细地解释。我从你的回答中学到了很多。它有效。我有这个问题,有没有办法在 Mockito 中使用 match 方法来做到这一点,而无需在我正在测试的类中触摸/添加任何额外的东西?只是好奇是否有 match() 的方法。就像忽略除 DateTime 之外的所有内容。
  • @Amiko 我想不出一个好的方法来使用matches 而不将您的预期查询本身变成一个巨大且不可读的正则表达式(尽管您可以使用转义工具构建查询表达式,例如,并在您编写的表达式中连接)。这仍然更难阅读和维护,并且不会测试正确的日期是否是您查询的一部分。另一种解决方案是使用 ArgumentCaptor 捕获查询并对其进行解析或删除日期,具有类似的可读性/可维护性/覆盖成本。在任何情况下我都不推荐任何一种解决方案。
【解决方案2】:

似乎您是在编写代码之后编写测试:p(否则您永远不会遇到这样的问题)。

我要做的是将 localedatetime 传递给函数,以便您可以正确地对其进行单元测试:

public List<DrawResultDto> getLastDrawResult(LocalDateTime t, Optional<Long> lotteryId, Optional<String> name, Optional<String> byName, Optional<String> byDate, Optional<Boolean> jackpotOnly) {
    String query = QUERY_MAIN_BLOCK.replace("/*statement0*/", t.plusMinutes(buyingLock).toString()) +
    [...]
}

为了与现有代码保持一致,请保留实际签名:

public List<DrawResultDto> getLastDrawResult(Optional<Long> lotteryId, Optional<String> name, Optional<String> byName, Optional<String> byDate, Optional<Boolean> jackpotOnly) {
    return getLastDrawResult(LocaleDateTime.now(), Optional<Long> lotteryId, Optional<String> name, Optional<String> byName, Optional<String> byDate, Optional<Boolean> jackpotOnly);
}

现在您可以正确地对第一种方法进行单元测试(通过传递一个内置在测试方法中的 localedatetime),对第二种方法进行单元测试很简单。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-12
    • 2021-09-16
    • 1970-01-01
    • 2010-10-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多