简体   繁体   中英

How to unit test a timer class (adapter pattern)?

I'm currently abstracting the concept of timers so that my classes that need one can use mock timers in tests or different implementations in operative mode (eg threadpool timers, thread-affine timers, etc.). Therefore, I created this interface:

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

    void Start();
    void Stop();

    event EventHandler IntervalElapsed;
}

Now I want to create a wrapper that adapts the System.Threading.Timer class and implements that interface. I want to do it using test-driven development. My class currently looks somewhat like this:

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

My actual question is: how would you write unit tests that describe the (soft real-time) requirements for the behavior of Start , Stop and IntervalElapsed ?

In my opinion, i should use eg an AutoResetEvent and check if the event is raised within a certain timespan (maybe +/- 3ms). But writing that code somewhat violates the DAMP (descriptive and meaningful phrases) principle, I think. Is there an easier way to do this?

Should I make the dependency to System.Threading.Timer external and then maybe use a shim for testing purposes? Unfortunately, the .NET timers do not have a common interface (which would make my work obsolete...)

What are your thoughts on that topic? Is there any documentation that I have not found yet and that I should read?

Sorry for having actually more than one question in this post, but this testing of soft real-time requirements is quite interesting, I think.

As no one answered this question yet, I'll tell you how I approached the problem: I used the spy pattern to actually implement the code that observes the behavior of the timer. The class looks like this:

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

This class takes a ThreadPoolTimer and registers to its IntervalElapsed event. One can specify how much intervals the spy should wait until it stops measuring. As I'm using a ManualResetEvent to block the thread that starts the timer in the Measure method, all calls to that method are synchronous, which results in DAMP code in the actual test class, in my opinion.

A test method that uses the spy would look like this:

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

If you have any questions or recommendations, please feel free to leave a comment.

Besides: the tolerance interval I have to choose to make the tests pass are relatively high. I thought that maybe 3 to 5 milliseconds might suffice, but in the end for ten intervals I figured out that the actual measured time span is up to 72ms different than the expected time of 1000ms in this case. Well, never use a managed runtime for real time applications, I guess...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM