Unit testing a class that uses a Timer

二次信任 提交于 2019-11-29 02:50:30
  1. You are not testing methods (private or public) - you are verifying behavior of your class. And if you have not verified some behavior, then you can't tell it was implemented. There are several ways this behavior could be invoked - public interface of your class, or some event in dependency. Also not necessary that behavior invocation will change something reached by the public interface, interactions with dependencies also matter.
  2. See example below - it shows how to test such "hidden" behavior.
  3. See example below - it shows how to split responsibilities, inject dependencies and mock them.

Actually your class have too many responsibilities - one is scheduling some task, and another - executing some actions. Try to split your class into two separate classes with single responsibilities.

So, scheduling goes to scheduler :) API of scheduler could be like:

public interface IScheduler
{
    event EventHandler<SchedulerEventArgs> Alarm;
    void Start();
    void Stop();
}

Forget about scheduler for now. Return and implement your second class, which will display some warnings. Let's go test first (with Moq):

[Test]
public void ShouldStopDisplayingWarningsWhenTimeIsOut()
{
    Mock<IDisplay> display = new Mock<IDisplay>();
    Mock<IScheduler> scheduler = new Mock<IScheduler>();                      

    Foo foo = new Foo("Bar", scheduler.Object, display.Object);
    scheduler.Raise(s => s.Alarm += null, new SchedulerEventArgs(0));

    display.Verify(d => d.Execute("Bar", WarningState.Ending, null));
    scheduler.Verify(s => s.Stop());
}

Write implementation:

public class Foo
{
    private readonly IScheduler _scheduler;
    private readonly IDisplay _display;
    private readonly string _name;

    public Foo(string name, IScheduler scheduler, IDisplay display)
    {
        _name = name;
        _display = display;
        _scheduler = scheduler;
        _scheduler.Alarm += Scheduler_Alarm;
        _scheduler.Start();
    }

    private void Scheduler_Alarm(object sender, SchedulerEventArgs e)
    {
        _display.Execute(_name, WarningState.Ending, null);
        _scheduler.Stop();
    }
}

Test passes. Write another one:

[Test]
public void ShouldNotStopDisplayingWarningsWhenTimeRemains()
{
    Mock<IDisplay> display = new Mock<IDisplay>(MockBehavior.Strict);
    Mock<IScheduler> scheduler = new Mock<IScheduler>(MockBehavior.Strict);
    scheduler.Setup(s => s.Start());

    Foo foo = new Foo("Bar", scheduler.Object, display.Object);
    scheduler.Raise(s => s.Alarm += null, new SchedulerEventArgs(1));
}

Test failed. Ah, you need condition for remaining time:

private void Scheduler_Alarm(object sender, SchedulerEventArgs e)
{
    if (e.RemainingTime > 0)
        return;

    _display.Execute(_name, WarningState.Ending, null);
    _scheduler.Stop();
}

You can continue writing tests for your class, which responsible for handling scheduler alerts and executing some warnings on display. When you finish, you can write implementation for your IScheduler interface. It does not matter how you will implement scheduling - via System.Windows.Forms.Timer or via System.ThreadingTimer, or some other way.

Is it worth testing the method? (since it's private)

Your purpose is to decide whether your code works or not. Even it's a private method, it should generate an output that can be reached by the public interface. You should design your class in a way that the user can know if it's working or not.

Also when you are unit testing, the callback assigned to Elapsed event of the timer is reachable if you can mock the timer.

How can I test it? (I know I can have my test class inheriting the class I want to test...)

You can use an adapter class here. First you have to define an abstraction since Timer class doesn't offer one.

public interface ITimer
{
    void Start();
    void Stop();
    double Interval { get; set; }
    event ElapsedEventHandler Elapsed;
    //and other members you need
}

Then you can implement this interface in an adapter class, just inheriting from the Timer class.

public class TimerAdaper : Timer, ITimer { }

You should inject your abstraction in the constructor(or as a property) so you can mock it in your tests.

public class MyClass
{
    private readonly ITimer _timer;

    public MyClass(ITimer timer)
    {
        _timer = timer
    }
}

Should I be mocking my timer? Because if I have to test a class that uses an internal timer, my tests may take a lot of time to complete, right?

Of course you should be mocking your timer. Your unit tests cannot depend on system time. You should raise events by mocking and see how your code behaves.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!