比较 DateTimes 的 JUnit 测试方法仅在来自套件的 运行 时失败

JUnit test method comparing DateTimes fails only when run from suites

在 JUnit 4.11 下 运行 的单个 JUnit 测试在大多数情况下都失败了,而通过任一模块测试套件 运行 (40 运行s : 2 次失败,38 次通过),或 class 测试套件(40 运行s:6 次失败,34 次通过),但是 运行测试方法本身并没有产生一次失败(50 运行秒:0 次失败,50 次通过)。

总结一下正在发生的事情,equals(Object MyObject) 实现 returns true 如果 org.joda.time.DateTime 对应于键 Stamp.START 或键 Stamp.STOP 对于当前实例与传递给该方法的实例相同。这是代码:

import org.joda.time.DateTime;
...
private final Map<Stamp, DateTime> timeStampMap;
...
@Override
public boolean equals(Object obj) {
    if (this == obj) { return true; }
    if (obj == null || getClass() != obj.getClass()) { return false; }
    final MyObject other = (MyObject) obj;
    return (Objects.equals(this.timeStampMap.get((Stamp.START)),
                           other.timeStampMap.get(Stamp.START))
            && Objects.equals(this.timeStampMap.get(Stamp.STOP),
                              this.timeStampMap.get(Stamp.STOP)));
}
...
public enum Stamp {
    START,
    STOP
}

测试本身:

@Test
@Config(configuration = TestConfig.NO_CONFIG)
public void equalityTest() {
    MyObject a = new MyObject(BigDecimal.TEN);
    MyObject b = a;

    assertThat(a.hashCode(), is(b.hashCode()));
    assertTrue(a.equals(b));

    b = new MyObject(BigDecimal.TEN);

    // This line produces the failure
    assertThat(a, is(not(b)));
}

为什么这个测试只在 运行 在任一测试套件下失败,而不是在 运行 单独时失败?

在尝试生成 MCVE 和编写问题的过程中,我发现了一些有趣的东西:

当测试在方法级别 运行 时,请注意 1 毫秒的时间戳差异。差距绝不会小于:

[START: 2015-02-26T11:53:20.581-06:00, STOP: 2015-02-26T11:53:20.641-06:00, DURATION: 0.060]    
[START: 2015-02-26T11:53:20.582-06:00, STOP: 2015-02-26T11:53:20.642-06:00, DURATION: 0.060]

但是当我 运行 测试最终成为 运行 作为套件的一部分时,几乎每次都会发生这种情况:

[START: 2015-02-26T12:25:31.183-06:00, STOP: 2015-02-26T12:25:31.243-06:00, DURATION: 0.060]
[START: 2015-02-26T12:25:31.183-06:00, STOP: 2015-02-26T12:25:31.243-06:00, DURATION: 0.060]

零差异。奇怪吧?

我最好的猜测 众所周知,JVM 在达到此特定测试时已经全部预热并且已经建立了一些动力运行测试套件时。如此之多,以至于实例化发生得如此之快,几乎是同时发生的。从 MyObject a 被实例化到 b 被分配到 b 被重新分配为一个新的 MyObject 之间经过的时间非常短,以至于产生 MyObject 与一对相同的 DateTimes.

事实证明,有一些可用的解决方案:

我采用的解决方案:

这与重新分配 MyObject b 之前的 . Call DateTimeUtils.setCurrentMillisOffset(val) 非常相似,然后在之后立即重置,因为我只需要足够长的偏移量来强制 DateTimes 之间的差异 MyObjects ab:

@Test
@Config(configuration = TestConfig.NO_CONFIG)
public void equalityTest() {
    MyObject a = new MyObject(BigDecimal.TEN);
    MyObject b = a;

    assertThat(a.hashCode(), is(b.hashCode()));
    assertTrue(a.equals(b));

    // Force an offset
    DateTimeUtils.setCurrentMillisOffset(1000);
    b = new MyObject(BigDecimal.TEN);
    // Clears the offset
    DateTimeUtils.setCurrentMillisSystem();

    assertThat(a, is(not(b)));
}

在整个项目 and/or 实际使用中可能会出现此问题的情况下,这很容易成为最佳解决方案。

通过在单元测试开始时调用 DateTimeUtils.setCurrentMillisFixed(val) 将当前时间设置为 return 固定时间,然后通过调用 DateTimeUtils.setCurrentMillisFixed(val + someOffset) 强制重新分配前的差异 MyObject b。单击 link 可直接跳转到他的代码解决方案。

值得指出的是,您需要在某个时候调用 DateTimeUtils.setCurrentMillisSystem() 来重置时间,否则依赖于时间的其他测试可能会受到影响。

原解:

我认为这里值得一提的是,据我了解,这是唯一不依赖于在父系统上具有某些安全权限的程序的解决方案。

调用 Thread.sleep() 确保两个 MyObjects:

DateTime 时间戳之间有时间间隔
@Test
@Config(configuration = TestConfig.NO_CONFIG)
public void equalityTest() {
    MyObject a = new MyObject(BigDecimal.TEN);
    MyObject b = a;

    assertThat(a.hashCode(), is(b.hashCode()));
    assertTrue(a.equals(b));

    try {
        Thread.sleep(0, 1);
    } catch (Exception e) {
        e.printStackTrace();
    }
    b = new MyObject(BigDecimal.TEN);

    // Line that was failing
    assertThat(a, is(not(b)));
}

由于您使用的是 Joda 时间,因此另一种方法可能是使用 DateTimeUtils.setCurrentMillisFixed(val).

将当前时间固定为您选择的时间

例如:

@Test
@Config(configuration = TestConfig.NO_CONFIG)
public void equalityTest() {
    DateTimeUtils.setCurrentMillisFixed(someValue);

    MyObject a = new MyObject(BigDecimal.TEN);
    MyObject b = a;

    assertThat(a.hashCode(), is(b.hashCode()));
    assertTrue(a.equals(b));

    DateTimeUtils.setCurrentMillisFixed(someValue + someOffset);

    b = new MyObject(BigDecimal.TEN);

    // This line produces the failure
    assertThat(a, is(not(b)));
}

我建议使代码更易于测试。您可以传入一个名为 Clock:

的接口,而不是让代码直接获取日期
public interface Clock {
  DateTime now();
}

然后你可以将Clock添加到构造函数中:

MyObject(BigDecimal bigDecimal, Clock clock) {
  timeStampMap.put(Stamp.START, clock.now());
}

对于生产代码,您可以创建辅助构造函数:

MyObject(BigDecimal bigDecimal) {
  this(bigDecimal, new SystemClock());
}

...其中 SystemClock 看起来像这样:

public class SystemClock implements Clock {

  @Override
  public DateTime now() {
    return new DateTime();
  }
}

您的测试可以模拟 Clock 或者您可以创建一个假时钟实现。