views:

149

answers:

4

The fundamental question is how do I create a unit test that needs to call a method, wait for an event to happen on the tested class and then call another method (the one that we actually want to test)?

Here's the scenario if you have time to read further:

I'm developing an application that has to control a piece of hardware. In order to avoid dependency from hardware availability, when I create my object I specify that we are running in test mode. When that happens, the class that is being tested creates the appropriate driver hierarchy (in this case a thin mock layer of hardware drivers).

Imagine that the class in question is an Elevator and I want to test the method that gives me the floor number that the elevator is. Here is how my fictitious test looks like right now:

[TestMethod]
public void TestGetCurrentFloor()
{
    var elevator = new Elevator(Elevator.Environment.Offline);
    elevator.ElevatorArrivedOnFloor += TestElevatorArrived;

    elevator.GoToFloor(5);

    //Here's where I'm getting lost... I could block
    //until TestElevatorArrived gives me a signal, but
    //I'm not sure it's the best way

    int floor = elevator.GetCurrentFloor();

    Assert.AreEqual(floor, 5);
}

Edit:

Thanks for all the answers. This is how I ended up implementing it:

    [TestMethod]
    public void TestGetCurrentFloor()
    {
        var elevator = new Elevator(Elevator.Environment.Offline);
        elevator.ElevatorArrivedOnFloor += (s, e) => { Monitor.Pulse(this); };

        lock (this)
        {
            elevator.GoToFloor(5);

            if (!Monitor.Wait(this, Timeout))
                Assert.Fail("Elevator did not reach destination in time");

            int floor = elevator.GetCurrentFloor();

            Assert.AreEqual(floor, 5);
        }
    }
+2  A: 

I think you are on the right lines already. The test needs to wait until either the event happens or you judge that it has taken too long to arrive and should give up waiting.

To do this, you could use Monitor.Wait with a timeout in your test and have it signalled with Monitor.Pulse when the event arrives.


[TestMethod]
public void TestGetCurrentFloor()
{
    var elevator = new Elevator(Elevator.Environment.Offline);
    elevator.ElevatorArrivedOnFloor += TestElevatorArrived;

    lock (this)
    {
        elevator.GoToFloor(5); // NOTE: this must hand off to a second thread, and the ElevatorArrivedOnFloor must be raised by this other thread otherwise the Monitor will be pulse before we've started waiting for it

        if (!Monitor.Wait(this, TIMEOUT)) Assert.Fail("Event did not arrive in time.");
    }

    int floor = elevator.GetCurrentFloor();

    Assert.AreEqual(floor, 5);
}

private void TestElevatorArrived(int floor)
{
    lock (this)
    {
        Monitor.Pulse(this);
    }
}

(The Assert.Fail() call here should be replaced with whatever mechanism your unit-testing tool uses for explicitly failing a test — or you could throw an exception.)

Paul Ruane
That's how I'm doing, except I'm doing it with a WaitHandle and a lambda instead of a method assigned to the event, but the idea is exactly the same.
Padu Merloti
Hi, this should solve a problem I have testing an aysnchronous operation. I used your code above as a starting point, however the Monitor.Pulse does not cause the Wait to reacquire the lock and it times out causing the Assert.Fail("Event did not arrive") to fire. Any ideas why this may be...!?
Kildareflare
@Kildareflare: the pulse causes the Monitor.Wait() to end waiting. Note that when the Monitor.Wait() ends waiting (because of a pulse or because it has timed out) it has to reacquire the lock, so it could be that the Monitor.Wait() had already timed but you see the message later when it is able to get the lock back. My guess would be you have set the timeout too small: it is in milliseconds, are you specifying seconds instead?
Paul Ruane
@Paul. Timeout is in milliseconds (5000). Takes elevator 250ms per floor. What happens is the elevator gets to floor 5 in just over a second and fires the elevator arrived event. The test then sits there until the Monitor.Wait timesout causes a test fail. As an aside i can get the alternate version using ManualResetEvent below to work.
Kildareflare
@Kildareflare: I think I know what is going wrong. Your implementatino of GoToFloor(): I bet you are using this to raise the ElevatorArrivedOnFloorEvent? If so, it will be using the same thread that will subsequently be waiting for the event. Follow the thread through: it will (1) get the lock, (2) call the GoToFloor() method, (3) raise the event which translates to a call to TestElevatorArrived, (4) pulse the monitor, (5) return from the TestElevatorArrived() method, (6) return from the GoToFloor() call, (7) begin waiting for a pulse which it has already missed.
Paul Ruane
@Kildareflare: For this code to work you will need to get the elevator to move, and raise the event asynchronously, i.e. using a separate thread. I have added a comment to the code to emphasise this.
Paul Ruane
+2  A: 

Maybe it's just a poor example, but your elevator sounds more like a state machine than something that's just processing asynchronously.

So your first set of tests could test that GoToFloor() will set the state to moving and that the direction it's moving in is correct.

Then the next set of tests would be on TestElevatorArrived(), and would test that if your state was moving towards a certain floor, that the actual movement (ie, the function that's called after the asynchronous wait, or the handler for the hardware firing a 'moved' event) will set the state to the expected floor.

Otherwise, what you're testing is most likely that your mocking of the hardware is correctly mocking the timing and moving, which doesn't seem correct.

Tanzelax
You are correct, my elevator does implement the state pattern and I'm already doing those sort of tests. I just got confused if waiting was the best thing to do in this specific case.
Padu Merloti
Ah, for this specific case, I'd say no. Didn't want to blanket say no because there's possibly (probably?) some good cases for testing with timers/timeouts and whatnot. But here, what you're "waiting" on would be your hardware to do some work, which is not what you're trying to unit test.
Tanzelax
+1  A: 

This is my similar approach.

    [TestMethod]
    public void TestGetCurrentFloor()
    {
        var completedSync = new ManualResetEvent(false);
        var elevator = new Elevator(Elevator.Environment.Offline);

        elevator.ElevatorArrivedOnFloor += delegate(object sender, EventArgs e)
        {
            completedSync.Set();
        };

        elevator.GoToFloor(5);

        completedSync.WaitOne(SOME_TIMEOUT_VALUE);

        int floor = elevator.GetCurrentFloor();

        Assert.AreEqual(floor, 5);
    } 

You can also test the return value of the WaitOne() call to check that your event handler was called.

Andrew Bienert
A: 

I really don't like the race-condition with the Monitor.Pulse/Wait method above.

A not-so-good but effective way would be something like the following:

[TestMethod]
public void TestGetCurrentFloor()
{
    // NUnit has something very similar to this, I'm going from memory
    this.TestCounter.reset(); 

    var elevator = new Elevator(Elevator.Environment.Offline);
    elevator.ElevatorArrivedOnFloor += (s,e) => { Assert.That(e.floor).Is(5) }

    elevator.GoToFloor(5);

    // It should complete within 5 seconds..
    Thread.Sleep(1000 * 5);
    Assert.That(elevator.GetCurrentFloor()).Is(5);

    Assert.That(this.TestCounter.Count).Is(2);
}

I don't like this solution because if the elevator arrives within 500ms, you're left waiting another 4500ms. If you had many tests like this, and you wanted your tests to be quick, I would totally avoid this scenario. However, this kind of test also doubles as a performance/sanity check.

Want to make sure that the elevator arrives within 2 seconds? Change the timeout.

Josh Smeaton