【问题标题】:Unit testing a class that uses a Timer对使用 Timer 的类进行单元测试
【发布时间】:2012-07-14 19:12:21
【问题描述】:

我有一个类,它有一个类型为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);
    }
}

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

【问题讨论】:

  • 你想验证什么行为?
  • 实际方法本身是否对时间有任何依赖性?你的单元测试应该测试方法的功能;它被定期调用的事实不应该改变您想要测试该方法是否有效的事实。我会说绝对没有必要模拟计时器,除非方法的成功取决于时间。当涉及到测试时,方法的可访问性应该无关紧要。它是您程序的功能部分。
  • +1,因为我输入了完全相同的问题进行私人领域测试,你按时击败了我
  • 值得测试的是您可以更好地处理的事情。有风险吗?会失败吗?一般准则是通过使用它们的公共方法来测试私有方法。最后,如果时间方面/角色难以测试/不可预测,您应该模拟它。

标签: c# unit-testing tdd


【解决方案1】:

是否值得测试该方法? (因为它是私人的)

您的目的是决定您的代码是否有效。即使它是一个私有方法,它也应该生成一个公共接口可以访问的输出。您应该以用户可以知道它是否工作的方式设计您的类。

此外,当您进行单元测试时,如果您可以模拟计时器,则分配给计时器的 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
    }
}

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

当然,你应该嘲笑你的计时器。您的单元测试不能依赖于系统时间。您应该通过模拟来引发事件并查看代码的行为方式。

【讨论】:

  • 请您详细说明 Private 是否值得测试,然后如何测试它们
  • @HatSoft 如果类的公共接口无法访问一段逻辑(公共或私有),则代码已死(无法使用)。因此,我们应该始终能够测试类中的所有代码。
  • @HatSoft 即使附加到计时器的经过事件的代码是私有的,它启动的部分也应该在一些公共成员中(可能是构造函数)。这使得私人部分可以访问。如果没有上下文,我不能说你如何测试它,但你应该并且可以测试它。
  • @dtryon & Ufuk 谢谢你能分享一些关于如何测试私人的链接
  • @HatSoft 这不是直接测试私人的逻辑。它是关于找到行使私有功能的公共流程,然后制作使用此流程的测试。
【解决方案2】:
  1. 您不是在测试方法(私有或公共) - 您是在验证您的班级的行为。而且,如果您还没有验证某些行为,那么您就无法判断它是否已实施。可以通过多种方式调用此行为 - 类的公共接口,或某些依赖事件。行为调用也不一定会改变公共接口所达到的内容,与依赖项的交互也很重要。
  2. 请参见下面的示例 - 它显示了如何测试这种“隐藏”行为。
  3. 请参见下面的示例 - 它展示了如何拆分职责、注入依赖项并模拟它们。

实际上你的班级有太多的责任 - 一个是安排一些任务,另一个 - 执行一些动作。尝试使用single responsibilities 将您的班级分成两个单独的班级。

所以,调度转到调度器 :) 调度器的 API 可能是:

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

暂时忘记调度程序。返回并实现你的第二个类,这将显示一些警告。让我们先测试一下(起订量):

[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 或其他方式。

【讨论】:

  • 我们如何为调度器实现编写单元测试呢? :)
  • @steavy 肯定是简单的集成测试 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-05-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-21
  • 1970-01-01
相关资源
最近更新 更多