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

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

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

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类并实现该接口(interface)。我想使用测试驱动开发来做到这一点。我的类(class)目前看起来有点像这样:

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

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

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

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

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

很抱歉在这篇文章中提出了不止一个问题,但我认为这种对软实时需求的测试非常有趣。

最佳答案

由于还没有人回答这个问题,我将告诉您我是如何解决这个问题的:我使用 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 事件。可以指定 spy 在停止测量之前应等待的时间间隔。当我使用 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 毫秒。好吧,我猜永远不要将托管运行时用于实时应用程序......

关于c# - 如何对计时器类(适配器模式)进行单元测试?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16936646/

相关文章:

c# - 我什么时候可以在 native foreach 循环上使用 List<T>.ForEach?

c# - 无法弄清楚c#中的多线程问题

c# - 有没有更好的方法来编写以下 Linq 查询

javascript - 在开 Jest 中使用 expo sqlite 调用单元测试类

c# - 需要一些帮助来破译来自 .NET JITted 代码的一行汇编代码

c# - 基于 bool 值重新使用 LINQ 查询

jquery table dnd 插件与 gridview - 放置事件永远不起作用

c# - LINQ to Nhibernate 重复连接

unit-testing - 如何对朴素贝叶斯词分类器进行单元测试?

unit-testing - 模拟命令并根据调用模拟的次数获得不同的结果