使用计时器的类进行单元测试

45

我有一个类,其中包含一个私有成员,类型为System.Windows.Forms.Timer。还有一个私有方法,每次计时器滴答时都会被调用。

  1. 即使是私有的,测试这个方法是否值得?
  2. 如何测试它?(我知道可以让我的测试类继承我想要测试的类...)
  3. 我应该模拟我的计时器吗?因为如果我必须测试使用内部计时器的类,我的测试可能需要很长时间才能完成,对吗?

编辑:

实际上,这个方法依赖于定时,以下是代码:

private void alertTick(object sender, EventArgs e) {
    if (getRemainingTime().Seconds <= 0) {
        Display.execute(Name, WarningState.Ending, null);
        AlertTimer.Stop();
    }
    else {
        var warning = _warnings.First(x => x == getRemainingTime());

        if (warning.TotalSeconds > 0)
            Display.execute(Name, WarningState.Running, warning);
    }
}

正如您所看到的,如果计时器正在运行,则会使用与结束时(剩余时间等于0时)不同的参数调用Display.execute()。这是否是设计上的问题?


你想要验证什么行为? - Robert Harvey
2
实际方法本身是否对时序有依赖性?您的单元测试应该测试该方法的功能;它被定期调用的事实不应改变您想要测试该方法是否有效的事实。我会说,除非该方法的成功取决于时间,否则绝对不需要模拟计时器。当涉及到测试时,方法的可访问性不应成为问题;它是程序的功能部分。 - dash
+1 因为我正在输入完全相同的私有字段测试问题,你比我更快。 - HatSoft
1
值得测试的是你更好地掌握的东西。它是否有风险?会失败吗?一般的准则是通过调用私有方法的公共方法来测试私有方法。最后,如果时间方面/角色使测试变得困难/不可预测,应该模拟它。 - Gishu
2个回答

51
  1. 您不是在测试方法(私有或公共)- 您正在验证类的行为。如果您没有验证某些行为,则无法告诉它已实现。有几种方式可以调用此行为 - 类的公共接口或依赖项中的某个事件。还不一定是行为调用将更改通过公共接口访问的内容,与依赖项的交互也很重要。
  2. 请参见下面的示例-它显示了如何测试这种“隐藏”行为。
  3. 请参见下面的示例-它显示了如何分割职责,注入依赖项并模拟它们。

实际上,您的类具有太多的职责-一个是安排某些任务,另一个是执行某些操作。尝试将您的类拆分为两个具有单个职责的单独类。

因此,调度进入调度程序:)调度程序的API可能如下:

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

暂时不考虑调度器。回到实现你的第二个类,它将显示一些警告。让我们先进行测试(使用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());
}

编写实现:

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

测试失败。啊,你需要一个剩余时间的条件:

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

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

你可以继续为处理调度器警报和在显示上执行某些警告的类编写测试。完成后,您可以编写IScheduler接口的实现。无论您如何实现调度 - 通过System.Windows.Forms.Timer或System.ThreadingTimer,或其他方式,都没有关系。

4
我们该如何为调度程序的实现编写单元测试呢? :) - steavy
@steavy 简单的集成测试肯定没问题 :) - Boltyk

28
是否值得测试这个方法?(因为它是私有的)
你的目的是决定你的代码是否工作。即使它是一个私有方法,它也应该生成一个可以通过公共接口访问的输出。您应该设计您的类,在用户可以知道它是否工作的情况下。另外,在单元测试时,如果您可以模拟计时器,分配给计时器Elapsed事件的回调是可达的。
如何测试它?(我知道我可以让我的测试类继承我想要测试的类...)
在这里,您可以使用适配器类。首先,您必须定义一个抽象类,因为Timer类没有提供一个。
public interface ITimer
{
    void Start();
    void Stop();
    double Interval { get; set; }
    event ElapsedEventHandler Elapsed;
    //and other members you need
}

接下来你可以在一个适配器类中实现这个接口,只需继承Timer类即可。

public class TimerAdaper : Timer, ITimer { }

你应该将抽象注入到构造函数中(或作为属性),这样你就可以在测试中模拟它。

public class MyClass
{
    private readonly ITimer _timer;

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

我应该模拟我的计时器吗?因为如果我必须测试一个使用内部计时器的类,我的测试可能需要很长时间才能完成,对吧?

当然应该模拟计时器。你的单元测试不能依赖于系统时间。你应该通过模拟来触发事件并查看你的代码如何运行。


请问如果私有变量值得测试,那么如何测试它们? - HatSoft
3
如果一个类的公共接口无法访问某个逻辑(公共或私有),那么这段代码就是不可用的(dead code)。因此,我们应该能够测试类中的所有代码。 - Davin Tryon
@HatSoft 即使与定时器的已过去事件相关联的代码是私有的,启动它的部分应该位于某个公共成员(可能是构造函数)中。这使得私有部分可访问。没有上下文,我无法说您如何测试它,但您应该并且可以测试它。 - Ufuk Hacıoğulları
@dtryon和Ufuk,谢谢你们,请问可以分享一些有关如何测试私有函数的链接吗? - HatSoft
5
@HatSoft 这并不是直接测试私有逻辑的问题。而是要找到公共流程来运用私有功能,然后制定一个使用该流程的测试。 - Davin Tryon
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接