如何运行分配给模拟对象的事件处理程序?

17

我正试图触发分配给我的计时器模拟的事件处理程序。我该如何测试这个私有方法?

public interface ITimer
{
    void Start();
    double Interval { get; set; }
    event ElapsedEventHandler Elapsed;
}

客户端类为此对象分配了一个事件处理程序。我想测试这个类中的逻辑。

_timer.Elapsed += ResetExpiredCounters;

所分配的方法是私有的

private void ResetExpiredCounters(object sender, ElapsedEventArgs e)
{
    // do something
}

我想在我的 mock 中添加这个事件处理程序,并以某种方式运行它。我该如何做?

更新:

我意识到我在分配事件处理程序之前触发了事件。我已经纠正了这一点,但我仍然收到此错误:

System.ArgumentException : Object of type 'System.EventArgs' cannot be converted 
to type 'System.Timers.ElapsedEventArgs'.

我这样提高它:

_timer.Raise(item => item.Elapsed += null, ElapsedEventArgs.Empty);
或者
_timer.Raise(item => item.Elapsed += null, EventArgs.Empty);

两个都不起作用。

更新:

这是对我有效的方法。请注意,如果您试图向事件处理程序传递信息(如Jon在评论中指出的那样),则此方法将无效。我只是使用它来模拟System.Timers.Timer类的包装器。

_timer.Raise(item => item.Elapsed += null, new EventArgs() as ElapsedEventArgs);
最终,如果需要使用事件参数,这样做将毫无帮助,因为它将始终为空。但是,这是唯一的方法,因为ElapsedEventArgs只有一个内部构造函数。

“类型为'System.EventArgs'的对象无法转换为类型'System.Timers.ElapsedEventArgs'”- 有哪一部分不清楚? - vgru
即使我传递了 ElapsedEventArgs ,我仍然会遇到那个错误。 我该如何触发该事件? - Ufuk Hacıoğulları
你的 ITimer 接口中的 ElapsedEventHandler 确实是 System.Timers.ElapsedEventHandler 吗? - vgru
@Groo 是的。Moq是否拦截参数并创建新的EventArgs或其他内容? - Ufuk Hacıoğulları
1
这是一个不错的问题,但请注意被接受的答案并没有直接解决问题——'new EventArgs() as ElapsedEventArgs' 只是传递了 null,它相当于传递 '(ElapsedEventArgs)null'(正如 Jon 指出的那样)。也许你可以把这个放在一个答案里而不是更新里? - g t
4个回答

18

ElapsedEventArgs有一个私有构造函数,不能被实例化。

如果你使用:

timer.Raise(item => item.Elapsed += null, new EventArgs() as ElapsedEventArgs);

然后,处理程序将收到一个空参数并且失去其SignalTime属性:

private void WhenTimerElapsed(object sender, ElapsedEventArgs e)
{
    // e is null.
}

在某些情况下,您可能需要使用此参数。

为了解决这个问题并使其更具可测试性,我还创建了一个包装器用于 ElapsedEventArgs 并使接口使用它:

public class TimeElapsedEventArgs : EventArgs
{
    public DateTime SignalTime { get; private set; }

    public TimeElapsedEventArgs() : this(DateTime.Now)
    {
    }

    public TimeElapsedEventArgs(DateTime signalTime)
    {
        this.SignalTime = signalTime;
    }
}

public interface IGenericTimer : IDisposable
{
    double IntervalInMilliseconds { get; set; }

    event EventHandler<TimerElapsedEventArgs> Elapsed;

    void StartTimer();

    void StopTimer();
}
实现将仅触发其自己的事件,从实际定时器事件获取数据:
public class TimerWrapper : IGenericTimer
{
    private readonly System.Timers.Timer timer;

    public event EventHandler<TimerElapsedEventArgs> Elapsed;

    public TimeSpan Interval
    {
        get
        {
            return this.timer.Interval;
        }
        set
        {
            this.timer.Interval = value;
        }
    }

    public TimerWrapper (TimeSpan interval)
    {
        this.timer = new System.Timers.Timer(interval.TotalMilliseconds) { Enabled = false };
        this.timer.Elapsed += this.WhenTimerElapsed;
    }

    private void WhenTimerElapsed(object sender, ElapsedEventArgs elapsedEventArgs)
    {
        var handler = this.Elapsed;
        if (handler != null)
        {
            handler(this, new TimeElapsedEventArgs(elapsedEventArgs.SignalTime));
        }
    }

    public void StartTimer()
    {
        this.timer.Start();
    }

    public void StopTimer()
    {
        this.timer.Stop();
    }

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                this.timer.Elapsed -= this.WhenTimerElapsed;
                this.timer.Dispose();
            }

            this.disposed = true;
        }
    }
}

现在,您可以简化和改进此事件的模拟:

timer.Raise(item => item.Elapsed += null, new TimeElapsedEventArgs());

var yesterday = DateTime.Now.AddDays(-1);
timer.Raise(item => item.Elapsed += null, new TimeElapsedEventArgs(yesterday));

编写的代码更少,更易于使用,并且完全解耦框架。


1
我就是想不通为什么ElapsedEventArgs有一个私有构造函数,导致这种疯狂的情况。无论如何,感谢你们两个的指导。@JoanComasFdz,如果您展示TimerWrapper的完整实现将会很有帮助。最终我解决了问题,但花了几分钟的时间。 - Brett
1
这是一种精妙的软件设计。不要依赖System...Timer,因为这样会导致您的代码无法测试。很好的例子! - thomasgalliker

5

Moq快速入门指南中有一个关于事件的部分。我认为你会使用。

mock.Raise(m => m.Elapsed += null, new ElapsedEventArgs(...));

客户端类中分配的处理程序似乎没有运行?我有什么遗漏吗?我不应该说模拟保存处理程序吗? - Ufuk Hacıoğulları
@UfukHacıoğulları:我认为模拟将只记住添加的任何处理程序。我建议您先退出真正的代码,然后尝试使用示例事件进行实验,直到证明Moq的工作原理,然后再返回到您的真实代码。 - Jon Skeet
5
ElapsedEventArgs没有构造函数。它无法编译,但我可以将新的EventArgs强制转换为ElapsedEventArgs。我会用那段代码更新你的答案,可以吗? - Ufuk Hacıoğulları
实际上这是System.Timers.Timer的适配器。我没有实现任何东西。因为方法是密封的,所以我无法模拟它们。但是在一般情况下,我想你是正确的。 - Ufuk Hacıoğulları
2
如果您无法创建ElapsedEventArgs,那么很遗憾,您无法轻松测试触发事件时会发生什么。看起来ElapsedEventArgs的设计并不方便进行测试 :( - Jon Skeet
显示剩余4条评论

4

最近处理了这个问题,你可以使用反射构造一个ElapsedEventArgs:

    public ElapsedEventArgs CreateElapsedEventArgs(DateTime signalTime)
    {
        var e = FormatterServices.GetUninitializedObject(typeof(ElapsedEventArgs)) as ElapsedEventArgs;
        if (e != null)
        {
            var fieldInfo = e.GetType().GetField("signalTime", BindingFlags.NonPublic | BindingFlags.Instance);
            if (fieldInfo != null)
            {
                fieldInfo.SetValue(e, signalTime);
            }
        }

        return e;
    }

这样可以继续使用原始的ElapsedEventHandler委托。
var yesterday = DateTime.Now.AddDays(-1);
timer.Raise(item => item.Elapsed += null, CreateElapsedEventArgs(yesterday));

我不会在生产代码中使用这个,但这正是我需要用于我的单元测试。 - Rob VS

0

可以像这样做来包装你的Timer

public class FakeTimer : IMyTimer
{
    private event ElapsedEventHandler elaspedHandler;
    private bool _enabled;

    public void Dispose() => throw new NotImplementedException();

    public FakeTimer(ElapsedEventHandler elapsedHandlerWhenTimeFinished, bool startImmediately)
    {
        this.elaspedHandler = elapsedHandlerWhenTimeFinished;
        _enabled = startImmediately;
    }

    public void Start() => _enabled = true;
    public void Stop() => _enabled = false;
    public void Reset() => _enabled = true;

    internal void TimeElapsed()
    {
        if (this._enabled)
            elaspedHandler.Invoke(this, new EventArgs() as ElapsedEventArgs);
    }
}

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