【问题标题】:How to unit test a timer class (adapter pattern)?如何对计时器类(适配器模式)进行单元测试?
【发布时间】:2013-06-05 09:46:17
【问题描述】:

我目前正在抽象计时器的概念,以便我的类可以在测试中使用模拟计时器或在操作模式下使用不同的实现(例如线程池计时器、线程仿射计时器等)。因此,我创建了这个界面:

public interface ITimer : IDisposable
{
    bool IsEnabled { get; }
    bool IsAutoResetting { get; set; }
    TimeSpan Interval { get; set; }

    void Start();
    void Stop();

    event EventHandler IntervalElapsed;
}

现在我想创建一个适应System.Threading.Timer 类并实现该接口的包装器。我想使用测试驱动开发来做到这一点。我的班级目前看起来有点像这样:

public sealed class ThreadPoolTimer : ITimer
{
    private readonly Timer _timer;

    public bool IsEnabled { get; private set; }

    public bool IsAutoResetting { get; set; }

    public TimeSpan Interval { get; set; }

    public ThreadPoolTimer()
    {
        Interval = this.GetDefaultInterval();
        _timer = new Timer(OnTimerCallback);
    }

    public void Dispose()
    {
        _timer.Dispose();
    }

    public void Start()
    {

    }

    public void Stop()
    {

    }

    private void OnTimerCallback(object state)
    {
        OnIntervalElapsed();
    }

    public event EventHandler IntervalElapsed;

    private void OnIntervalElapsed()
    {
        var handler = IntervalElapsed;
        if (handler != null)
            handler(this, EventArgs.Empty);
    }
}

我的实际问题是:您将如何编写描述StartStopIntervalElapsed 行为的(软实时)要求的单元测试?

在我看来,我应该使用例如AutoResetEvent 并检查事件是否在特定时间跨度内引发(可能是 +/- 3 毫秒)。但我认为编写该代码有点违反 DAMP(描述性和有意义的短语)原则。有没有更简单的方法来做到这一点?

我是否应该将 System.Threading.Timer 的依赖项设为外部,然后可能使用 shim 进行测试?不幸的是,.NET 计时器没有通用接口(这会使我的工作过时......)

你对这个话题有什么看法?是否有任何我尚未找到但我应该阅读的文档?

很抱歉在这篇文章中实际上有多个问题,但我认为这种软实时需求测试非常有趣。

【问题讨论】:

  • 我不认为这是重复的,因为所有这些问题都涉及封装计时器的类以及如何模拟它,但没有一个使用 TDD 实现实际接口。关于此的其他问题是例如stackoverflow.com/questions/879971/…stackoverflow.com/questions/12045/…
  • 创建一个包装器感觉就像是一种气味。我认为框架中的计时器类可以保证工作。我会创建子类来测试主题以调用我的计时器处理程序或注入我自己的时间间隔来编写一些集成测试。
  • @bryanbcook 你是如何从中抽象出来的?正如我之前提到的,.NET 中的计时器都没有实现公共接口。不创建适配器(实际上是这个包装器)意味着您不能在需要计时器的类的单元测试中从中抽象出来。
  • 我的观点是,除非有一个非常有力和令人信服的抽象理由,否则可能不需要它。例如,如果没有 UI,UI 调度程序不会抽出后台任务,这可能是测试的问题。另一方面,定时器是相当有保证的,所以除非你动态地操作定时器或者需要用不同的计时策略将它换掉,否则我会质疑我为什么需要抽象。

标签: c# .net unit-testing tdd real-time


【解决方案1】:

由于还没有人回答这个问题,我将告诉你我是如何解决这个问题的:我使用 spy 模式来实际实现观察计时器行为的代码。该类如下所示:

public class ThreadPoolTimerSpy : IDisposable
{
    private readonly ThreadPoolTimer _threadPoolTimer;

    private int _intervalElapsedCallCount;

    private readonly ManualResetEvent _resetEvent = new ManualResetEvent(false);

    public int NumberOfIntervals { get; set; }

    public DateTime StartTime { get; private set; }
    public DateTime EndTime { get; private set; }

    public ThreadPoolTimerSpy(ThreadPoolTimer threadPoolTimer)
    {
        if (threadPoolTimer == null) throw new ArgumentNullException("threadPoolTimer");
        _threadPoolTimer = threadPoolTimer;
        _threadPoolTimer.IntervalElapsed += OnIntervalElapsed;
        NumberOfIntervals = 1;
    }

    public void Measure()
    {
        _intervalElapsedCallCount = 0;
        _resetEvent.Reset();
        StartTime = DateTime.Now;
        _threadPoolTimer.Start();

        _resetEvent.WaitOne();
    }

    private void OnIntervalElapsed(object sender, EventArgs arguments)
    {
        _intervalElapsedCallCount++;

        if (_intervalElapsedCallCount < NumberOfIntervals)
            return;

        _threadPoolTimer.Stop();
        EndTime = DateTime.Now;
        _resetEvent.Set();
    }


    public void Dispose()
    {
        _threadPoolTimer.Dispose();
        _resetEvent.Dispose();
    }
}

这个类接受一个ThreadPoolTimer 并注册到它的IntervalElapsed 事件。可以指定间谍应该等待多少时间间隔,直到它停止测量。由于我使用ManualResetEvent 来阻止在Measure 方法中启动计时器的线程,因此对该方法的所有调用都是同步的,这在我看来会导致实际测试类中的DAMP 代码。

使用 spy 的测试方法如下所示:

[TestInitialize]
public void InitializeTestEnvironment()
{
    _testTarget = new ThreadPoolTimerBuilder().WithAutoResetOption(true)
                                              .WithInterval(100)
                                              .Build() as ThreadPoolTimer;
    Assert.IsNotNull(_testTarget);
    _spy = new ThreadPoolTimerSpy(_testTarget);
}

[TestMethod]
public void IntervalElapsedMustBeRaisedExactlyTenTimesAfter1000Milliseconds()
{
    CheckIntervalElapsed(10, TimeSpan.FromMilliseconds(1000), TimeSpan.FromMilliseconds(100));
}

private void CheckIntervalElapsed(int numberOfIntervals, TimeSpan expectedTime, TimeSpan toleranceInterval)
{
    _spy.NumberOfIntervals = numberOfIntervals;
    _spy.Measure();
    var passedTime = _spy.EndTime - _spy.StartTime;
    var timingDifference = Math.Abs(expectedTime.Milliseconds - passedTime.Milliseconds);
    Assert.IsTrue(timingDifference <= toleranceInterval.Milliseconds, string.Format("Timing difference: {0}", timingDifference));
}

如果您有任何问题或建议,请随时发表评论。

另外:为了使测试通过,我必须选择的容差区间比较高。我认为可能 3 到 5 毫秒就足够了,但最后十个间隔我发现实际测量的时间跨度与这种情况下的 1000 毫秒的预期时间相差最多 72 毫秒。好吧,我猜永远不要将托管运行时用于实时应用程序...

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-01-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-11-30
    • 1970-01-01
    相关资源
    最近更新 更多