C#单元测试 - Thread.Sleep(x) - 如何模拟系统时钟

13

我需要测试一个在一段时间后执行一定量工作的方法。

while (running)
{
    ...
    // Work
    ...
    Thread.Sleep(Interval);
}

一个间隔时间被作为类的参数传递,所以我只需要传入0或1,但是如果不是这种情况,我想知道如何模拟系统时钟。

在我的测试中,我想简单地将时间向前推进Interval,并使线程唤醒。

我从未为执行线程编写过测试,我确信有一些要避免的问题 - 请随意详细说明您使用的方法。

谢谢!


除非您真的想改变时钟的值,否则您将无法获得您要寻找的结果。 - Kirk Woll
2
首选解决方案是使用服务来隔离时间功能,以便您可以在测试中模拟该服务。如果您无法这样做(通常是因为它是现有的代码库),那么另一种选择是使用类似 Moles 的框架来绕过静态调用:http://research.microsoft.com/en-us/projects/moles/ - Dan Bryant
2个回答

18

如果你不想测试线程实际上是否休眠,一种更直接的方法(并且是可行的)是拥有一个ISleepService。然后你可以将其模拟出来,在测试中不进行睡眠,但在生产代码中有一个导致Thread.Sleep的实现。

ISleepService sleepService = Container.Resolve<ISleepService>();

..

while (running)
{
    ...
    // Work
    ...
    sleepService.Sleep(Interval);
}

使用 Moq 的示例:

    public interface ISleepService
    {
        void Sleep(int interval);
    }

    [Test]
    public void Test()
    {
        const int Interval = 1000;

        Mock<ISleepService> sleepService = new Mock<ISleepService>();
        sleepService.Setup(s => s.Sleep(It.IsAny<int>()));
        _container.RegisterInstance(sleepService.Object);

        SomeClass someClass = _container.Resolve<SomeClass>();
        someClass.DoSomething(interval: Interval);

        //Do some asserting.

        //Optionally assert that sleep service was called
        sleepService.Verify(s => s.Sleep(Interval));
    }

    private class SomeClass
    {
        private readonly ISleepService _sleepService;

        public SomeClass(IUnityContainer container)
        {
            _sleepService = container.Resolve<ISleepService>();
        }

        public void DoSomething(int interval)
        {
            while (true)
            {
                _sleepService.Sleep(interval);
                break;
            }
        }
    }

更新

就设计/维护而言,如果更改“SomeClass”的构造函数或向使用该类的用户添加依赖注入点很麻烦,那么服务定位器类型的模式可以帮助解决这个问题,例如:

private class SomeClass
{
    private readonly ISleepService _sleepService;

    public SomeClass()
    {
        _sleepService = ServiceLocator.Container.Resolve<ISleepService>();
    }

    public void DoSomething(int interval)
    {
        while (true)
        {
            _sleepService.Sleep(interval);
            break;
        }
    }
}

太棒了 - 这很有道理 - Moq肯定也在路线图上,所以你的答案提供了一个简单的入门。 - gav

3

你不能真正地模拟系统时钟。

如果你需要改变像这样的代码的挂起行为,你需要重构它,以便不直接调用 Thread.Sleep()

我会创建一个单例服务,可以在应用程序测试时注入。这个单例服务必须包含方法,允许一些外部调用者(比如单元测试)能够取消睡眠操作。

或者,你可以使用 MutexWaitHandle 对象的 WaitOne() 方法,它有一个超时参数。这样你就可以触发互斥体来取消“睡眠”,或让它超时:

public WaitHandle CancellableSleep = new WaitHandle(); // publicly available

// in your code under test use this instead of Thread.Sleep()...
while( running ) {
    // .. work ..
    CancellableSleep.WaitOne( Interval ); // suspends thread for Interval timeout
}


// external code can cancel the sleep by doing:
CancellableSleep.Set(); // trigger the handle...

1
WaitHandle 是一种非常占用系统资源和性能的方式,用于提供替代 Thread.Sleep() 的方法。我不建议使用 WaitHandle,除非您有其他代码可以指导线程何时应该唤醒。 - Dan Bryant
1
实际上你可以使用系统调用拦截,但那绝对是杀鸡焉用牛刀。 - Tim Lloyd
1
@Dan Bryant:我不否认等待句柄比Thread.Sleep()作为一般实践更昂贵。然而,在单个约束场景中,这可能是有意义的——特别是如果您使用可注入服务(如我所提到的)仅在测试时才这样做。对于发布代码,人们可能会使用更轻量级的Sleep()调用。 - LBushkin
1
@chibacity:虽然您可以使用系统调用拦截,但这会影响所有依赖于系统时钟的代码 - 这可能会对未经测试的其他代码产生不利影响。 - LBushkin
1
@LBushkin 我完全同意。任何形式的挂钟时间操作都可能产生意想不到的后果。最好坚持逻辑时间并依赖事件排序。 :) - Tim Lloyd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接