How can I run the event handler assigned to a mock?
Asked Answered
O

5

17

I am trying to fire the event handler assigned to my timer mock. How can I test this private method here?

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

Client class assigns an event handler to this object. I want to test the logic in this class.

_timer.Elapsed += ResetExpiredCounters;

And the assigned method is private

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

I want to have this event handler in my mock and run it somehow. How can I do this?

Update:

I realized I was raising the event before I assigned the event handler. I corrected that but I still get this error:

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

I raise it like this:

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

or

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

Both won't work.

Update:

Here's the thing that worked for me. Note that it's not useful if you are trying to pass info to event handler like Jon pointed out in comments. I am just using it to mock the wrapper for System.Timers.Timer class.

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

In the end, this won't help at all if you need to use event arguments since it will be always null. However, it's the only way since ElapsedEventArgs has only an internal constructor.

Overword answered 20/1, 2012 at 11:49 Comment(5)
"Object of type 'System.EventArgs' cannot be converted to type 'System.Timers.ElapsedEventArgs'" - What part is unclear?Causative
I still get that error even if I pass ElapsedEventArgs . How can I raise that event?Labour
Is ElapsedEventHandler in your ITimer interface really System.Timers.ElapsedEventHandler?Causative
@Groo Yes. Is Moq intercepting the parameters and creating new EventArgs or something?Labour
+1 for the question, but beware the accepted answer does not directly solve the problem - the 'new EventArgs() as ElapsedEventArgs' just passes in null, it's equivalent to passing '(ElapsedEventArgs)null' (as Jon points out). Perhaps you could put this in an answer rather than an update?Lion
I
18

ElapsedEventArgs has a private constructor and can not be instantiated.

If you use:

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

Then the handler will recevie a null parameter and lose its SignalTime property:

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

You might want this parameter in some cases.

To solve this and make it more testable, I also created a wrapper for the ElapsedEventArgs, and made the interface use it:

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

The implementation will simply fire its own event getting the data from the real timer event:

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

Now, you can simplify and improve the mock of this event:

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

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

Less code to write, easier to work with and completely decoupled from the framework.

Invent answered 29/1, 2014 at 8:50 Comment(2)
I simply can't get my head around why ElapsedEventArgs has a private constructor, necessitating this craziness. Anyway, thanks to both of you for the guidance. @JoanComasFdz, it would be helpful if you show the full implementation of TimerWrapper. I eventually sorted it out, but it took a few minutes.Myo
This is wonderful software design. Don't depend on System...Timer since your code will become untestable. Good example!Duiker
I
4

The Moq QuickStart guide has a section on events. I think you'd use

mock.Raise(m => m.Elapsed += null, new ElapsedEventArgs(...));
Interstadial answered 20/1, 2012 at 11:57 Comment(9)
Assigned handler from client class doesn't seem to run? Am I missing something? Shouldn't I be saying to mock the save the handler?Labour
@UfukHacıoğulları: I believe the mock will just remember any handlers which are added. I would suggest that you start off by coming out of your real code, and just experiment with a sample event until you've proved how Moq works, then go back to your real code.Interstadial
It doesn't seem to run event handler at allLabour
@UfukHacıoğulları: Can you show a short but complete console app that shows that? That way I can download Moq and try it for myself...Interstadial
@UfukHacıoğulları: Okay - look at the code you've used (using EventArgs.Empty) then look at the code in my answer... spot the difference. The error being thrown is pretty clear, IMO. You need to provide an actual ElapsedEventArgs.Interstadial
ElapsedEventArgs has no constructor. It won't compile but I was able cast new EventArgs as ElapsedEventArgs. I am updating your answer with that code. Is that okay ?Labour
@UfukHacıoğulları: I've rolled back your change - it was inappropriate, as it would always end up passing null to the handler. How is your real code going to create an ElapsedEventArgs instance to pass to the handler? How were you expecting to test what the handler did with the data in the ElapsedEventArgs, if you couldn't construct one somehow?Interstadial
Actually it's an adapter for System.Timers.Timer. I'm not implementing anything. I couldn't mock methods because they were sealed. But I guess you are right in general case.Labour
@UfukHacıoğulları: If you can't create an ElapsedEventArgs, you can't easily test what happens when you fire an event, unfortunately. Looks like ElapsedEventArgs wasn't designed in a handy-for-testing way :(Interstadial
Y
4

Dealt with this recently, you can construct an ElapsedEventArgs using reflection:

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

This way you can continue using the original ElapsedEventHandler delegate

var yesterday = DateTime.Now.AddDays(-1);
timer.Raise(item => item.Elapsed += null, CreateElapsedEventArgs(yesterday));
Yolandayolande answered 25/4, 2017 at 19:39 Comment(1)
I would not use this in production code, but this is exactly what I need for my unit tests.Adrianople
A
1

The OP points out that they can simply use new EventArgs() as ElapsedEventArgs which is a nice simple way to get it done in one line, and assuming the object is never examined in the handler it will work - but it will generate a "possible null reference" build warning.

If you're aiming for zero build warnings then the best method for this use case, avoiding the obsolete classes in Yoyo's answer, is something like this:

private ElapsedEventArgs CreateElapsedEventArgs()
{
    return (ElapsedEventArgs)RuntimeHelpers.GetUninitializedObject(typeof(ElapsedEventArgs));
}
Abuttal answered 11/1 at 9:0 Comment(0)
G
0

Could do something like this to wrap your 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);
    }
}
Gyrus answered 13/9, 2021 at 16:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.