是否有可能在单元测试中模拟出时间?

时间:2009-08-21 00:46:17

标签: unit-testing time mocking

继续this问题......我正在尝试对以下场景进行单元测试:

我有一个类允许一个方法调用一个方法来执行一些操作,如果它失败则等待一秒钟并重新调用该方法。

说我想调用一个方法DoSomething()...但是如果DoSomething()抛出异常,我希望能够重试最多调用它3次但等待1秒之间每次尝试。在这种情况下,单元测试的目的是验证当我们调用DoSomething()3次,每次重试之间等待1秒钟时,所花费的总时间> = 3秒。

不幸的是,我能想到测试它的唯一方法就是使用秒表计时......它有两个副作用......

  1. 执行测试需要3秒......我通常喜欢我的测试以毫秒运行
  2. 运行测试的时间长度相差+/- 10ms左右,除非我考虑到这种差异,否则会导致测试失败。
  3. 如果有一种方法可以模拟出这种依赖性,那么我的测试能够更快地运行并且在结果中相当一致,那将是多么好的。不幸的是,我想不出办法这样做......所以我想我会问,看看你们中间是否有人遇到过这个问题......

5 个答案:

答案 0 :(得分:8)

您可以创建一个Waiter类,该类提供等待指定时间的方法Wait(int)。对此方法进行测试,然后在单元测试中传入一个模拟版本,该版本只是跟踪要求等待的时间,但会立即返回。

例如(C#):

interface IWaiter
{
    void Wait(int milliseconds);
}

class Waiter : IWaiter
{
    public void Wait(int milliseconds)
    {
        // not accurate, but for the sake of simplicity:
        Thread.Sleep(milliseconds);
    }
}

class MockedWaiter : IWaiter
{
    public void Wait(int milliseconds)
    {
        WaitedTime += milliseconds;
    }
    public int WaitedTime { get; private set; }
}

答案 1 :(得分:4)

诀窍是创建时间提供者的模拟。

在Ruby中,您只需使用模拟库:

now = Time.now Time.expects(:现在).returns(现在的)。一旦 Time.expects(:now).returns(现在+ 1).once 等

在Java中,它有点复杂。用您自己的“时钟”替换常规时间

interface Clock {
  Date getNow();
}

然后,重写:

public String elapsedTime(Date d) {
  final Date now = new Date();
  final long ms = now.getTime() - d.getTime();
  return "" + ms / 1000 + " seconds ago";
}

为:

public String elapsedTime(Date d, Clock clock) {
  final Date now = clock.getNow();
  final long ms = now.getTime() - d.getTime();
  return "" + ms / 1000 + " seconds ago";
}

显然,时钟可以通过其他方式注入,但现在我们有一个可测试且可扩展的方法。

答案 2 :(得分:2)

我经常测试这样的重试尝试。我使用的方法是使超时可配置 - 然后我可以在我的测试中将其设置为零。在你的情况下你可以模拟对象DoSomething()被调用,期望3次调用它并将超时设置为0秒 - 然后你可以验证DoSomething()被立即调用3次。

另一种方法是使用一个接口ITimer,在每次调用DoSomething()之间调用Wait(int秒) - 然后你可以模拟出ITimer并验证Wait(int)被调用3次正确的秒数参数。然后,ITimer的具体实现会执行Thread.sleep或用于等待的任何方法。

答案 3 :(得分:2)

我写了一系列关于在C ++中测试计时器类的博客文章。正如其他人所说的那样,关键是将时间提供者分开然后嘲笑它。一旦你完成了这一切,其余的都很容易。如果您有兴趣,请参见此处:http://www.lenholgate.com/blog/2004/05/practical-testing.html

答案 4 :(得分:1)

你的直觉是正确的。诀窍是将问题的不同部分分开,而不是将它们捆绑到一个类中。您需要一个实现DoSomething的Action对象,该对象与时间无关;一个Retryer对象,它将管理调用Action对象的尝试;和一个等待的时钟。您可以直接对Action对象进行单元测试,使用存根的Clock和Action对象对Retryer进行单元测试,并使Clock变得如此简单以至于它可以正常工作。

最重要的是,不要在测试中进行真正的睡眠,它太脆弱而且很慢。