如何对计时器类(适配器模式)进行单元测试?

时间:2013-06-05 09:46:18

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

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

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);
    }
}

我的实际问题是:您如何编写单元测试来描述StartStop和{{1}行为的(软实时)要求}}?

在我看来,我应该使用例如IntervalElapsed并检查事件是否在某个时间段内(可能是+/- 3毫秒)被提升。但我认为,编写该代码有点违反了DAMP(描述性和有意义的短语)原则。有更简单的方法吗?

我是否应该依赖AutoResetEvent外部,然后使用垫片进行测试?不幸的是,.NET计时器没有通用接口(这会使我的工作过时......)

您对该主题有何看法?有没有我尚未找到的文件,我应该阅读?

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

1 个答案:

答案 0 :(得分:0)

由于还没有人回答这个问题,我会告诉你我是如何处理这个问题的:我使用间谍模式来实际实现观察计时器行为的代码。该课程如下:

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代码,在我看来

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

[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毫秒就足够了,但最后10个时间间隔我发现实际测量的时间跨度比这种情况下1000ms的预期时间长达72ms。好吧,我从不使用托管运行时用于实时应用程序,我猜......