如何对这个库进行单元测试?

时间:2010-10-26 18:48:15

标签: c# multithreading unit-testing events

我有一个外部库,它有一个在后台线程上执行长时间运行任务的方法。完成后,它会触发启动方法的线程上的Completed事件(通常是UI线程)。它看起来像这样:

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

调用此库的代码如下所示:

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
}

如果在事件中返回数据,我如何对.LongRunningTask的调用进行单元测试?

4 个答案:

答案 0 :(得分:2)

嗯,我相信BackgroundWorker使用当前的SynchronizationContext。您可以实现自己的SynchronizationContext子类以允许更多控制(甚至可能在同一个线程上运行代码,尽管这会破坏依赖在其他线程中运行的)并在运行测试之前调用SetSynchronizationContext

您需要在测试中订阅该事件,然后检查您的处理程序是否被调用。 (Lambda表达式对此很有用。)

例如,假设您有一个SynchronizationContext,只允许您在需要时运行所有工作,并告诉您何时完成,您的测试可能会:

  • 设置同步上下文
  • 创建组件
  • 使用lambda订阅处理程序,该lambda设置本地变量
  • 致电LongRunningTask()
  • 验证尚未设置局部变量
  • 让同步上下文完成所有工作......等到它完成(超时)
  • 验证是否已设置局部变量

诚然,这有点令人讨厌。如果可以只是同步测试它正在进行的工作,那将会更容易。

答案 1 :(得分:2)

我不确定我是否做对了。如果它触发事件,你想检查外部库吗?或者你想检查你是否在事件被触发时做了什么?

如果是后者,我会使用模拟。但问题是,您的代码似乎很难测试,因为您在用户界面中执行逻辑操作。尝试写一个“被动”视图,让主持人做出魔术。例如,使用模型视图展示器模式http://msdn.microsoft.com/en-us/magazine/cc188690.aspx

整个事情就像这样。

模特

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

视图

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

演示者

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

你组装它的地方(例如在Program类中)

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

然后您可以轻松地在单元测试中测试演示者。 gui中的部分现在已经不足以进行测试了。

可能的测试看起来像这样

    [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"));
    } 

答案 2 :(得分:1)

您可以创建一个有助于将其转换为同步调用的扩展方法。您可以进行调整,例如使其更通用并传递超时变量,但至少它会使单元测试更容易编写。

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

答案 3 :(得分:1)

为了测试异步代码,我使用了类似的帮助器:

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

在测试方法中,然后调用类似的东西:

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