开发者

How to unit test this library?

I have an external library which has a method which performs a long running task on a background thread. When it's done it fires off a Completed event on the thread that kicked off the method (typically the UI thread). It looks like this:

public class Foo
{
开发者_如何学运维    public delegate void CompletedEventHandler(object sender, EventArgs e);
    public event CompletedEventHandler Completed;

    public void LongRunningTask()
    {
        BackgroundWorker bw = new BackgroundWorker();
        bw.DoWork += new DoWorkEventHandler(bw_DoWork);
        bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
        bw.RunWorkerAsync();
    }

    void bw_DoWork(object sender, DoWorkEventArgs e)
    {
        Thread.Sleep(5000);
    }

    void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (Completed != null)
            Completed(this, EventArgs.Empty);
    }
}

The code that calls this library looks like this:

private void button1_Click(object sender, EventArgs e)
{
    Foo b = new Foo();
    b.Completed += new Foo.CompletedEventHandler(b_Completed);
    b.LongRunningTask();

    Debug.WriteLine("It's all done");    
}

void b_Completed(object sender, EventArgs e)
{
    // do stuff
}

How do I unit test the call to .LongRunningTask given that it returns data in an event?


I'm not sure if I got it right. Do you want to check the external library if it fires the event? Or do you want to check that you do something particularly if the event is fired?

If it is the latter, I would use a mock for that. The problem is though, that your code seems to be hard to test, because you're doing logical stuff in the user interface. Try to write a "passive" view, and let a presenter do the magic. For example by using the Model View Presenter pattern http://msdn.microsoft.com/en-us/magazine/cc188690.aspx

The whole thing would then look like this.

The Model

public class Model : IModel
{
    public event EventHandler<SampleEventArgs> Completed;

    public void LongRunningTask()
    {
        BackgroundWorker bw = new BackgroundWorker();
        bw.DoWork += this.bw_DoWork;
        bw.RunWorkerCompleted += this.bw_RunWorkerCompleted;
        bw.RunWorkerAsync();

    }

    private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (this.Completed != null)
        {
            this.Completed(this, new SampleEventArgs {Data = "Test"});
        }
    }

    private void bw_DoWork(object sender, DoWorkEventArgs e)
    {
        System.Threading.Thread.Sleep(5000);
    }
}

The View

    public Form1()
    {
        InitializeComponent();
    }

    public event EventHandler Button1Clicked;

    public void Update(string data)
    {
        this.label1.Text = data;
    }

    private void Button1Click(object sender, EventArgs e)
    {
        if (this.Button1Clicked != null)
        {
            this.Button1Clicked(this, EventArgs.Empty);
        }
    }

The Presenter

public class Presenter
{
    private readonly IForm1 form1;
    private readonly IModel model;

    public Presenter(IForm1 form1, IModel model)
    {
        this.form1 = form1;
        this.model = model;

        this.form1.Button1Clicked += this.Form1Button1Clicked;
        this.model.Completed += this.ModelCompleted;
    }

    private void ModelCompleted(object sender, SampleEventArgs e)
    {
        this.form1.Update(e.Data);
    }

    private void Form1Button1Clicked(object sender, EventArgs e)
    {
        this.model.LongRunningTask();
    }
}

Somewhere you assemble it (e.g. in the Program class)

var form = new Form1();
var model = new Model();
var presenter = new Presenter(form, model);
Application.Run(form);

And then you can easily just test the presenter in an unit test. The part in the gui is now little enough to not be tested.

The possible test could look like this

    [Test]
    public void Test()
    {
        var form1Mock = new Mock<IForm1>();
        var modelMock = new Mock<IModel>();

        var presenter = new Presenter(form1Mock.Object, modelMock.Object);

        modelMock.Setup(m => m.LongRunningTask()).Raises(m => m.Completed += null, new SampleEventArgs() { Data = "Some Data" });

        form1Mock.Raise(f => f.Button1Clicked += null, EventArgs.Empty);

        form1Mock.Verify(f => f.Update("Some Data"));
    } 


Well, I believe BackgroundWorker uses the current SynchronizationContext. You could potentially implement your own subclass of SynchronizationContext to allow you more control (possibly even running code on the same thread, although that will break anything which depends on it running in a different thread) and call SetSynchronizationContext before running the test.

You'd need to subscribe to the event in your test, and then check whether or not your handler was called. (Lambda expressions are good for this.)

For example, suppose you have a SynchronizationContext which lets you run all the work only when you want it to, and tell you when it's done, your test might:

  • Set the synchronization context
  • Create the component
  • Subscribe to the handler with a lambda which sets a local variable
  • Call LongRunningTask()
  • Verify that the local variable hasn't been set yet
  • Make the synchronization context do all its work... wait until it's finished (with a timeout)
  • Verify that the local variable has now been set

It's all a bit nasty, admittedly. If you can just test the work it's doing, synchronously, that would be a lot easier.


You can create an extension method that can help with turning it into a synchronous call. You can make tweaks like making it more generic and passing in the timeout variable but at least it will make the unit test easier to write.

static class FooExtensions
{
    public static SomeData WaitOn(this Foo foo, Action<Foo> action)
    {
        SomeData result = null;
        var wait = new AutoResetEvent(false);

        foo.Completed += (s, e) =>
        {
            result = e.Data; // I assume this is how you get the data?
            wait.Set();
        };

        action(foo);
        if (!wait.WaitOne(5000)) // or whatever would be a good timeout
        {
            throw new TimeoutException();
        }
        return result;
    }
}

public void TestMethod()
{
    var foo = new Foo();
    SomeData data = foo.WaitOn(f => f.LongRunningTask());
}


For testing asynchronous code I use a similar helper:

public class AsyncTestHelper
{
    public delegate bool TestDelegate();

    public static bool AssertOrTimeout(TestDelegate predicate, TimeSpan timeout)
    {
        var start = DateTime.Now;
        var now = DateTime.Now;
        bool result = false;
        while (!result && (now - start) <= timeout)
        {
            Thread.Sleep(50);
            now = DateTime.Now;
            result = predicate.Invoke();
        }
        return result;
    }
}

In the test method then call something like this:

Assert.IsTrue(AsyncTestHelper.AssertOrTimeout(() => changeThisVarInCodeRegisteredToCompletedEvent, TimeSpan.FromMilliseconds(500)));
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜