单元测试从线程发射的事件

时间:2010-02-25 20:10:40

标签: c# .net unit-testing multithreading thread-safety

我遇到问题单元测试一个类,当一个线程启动并完成时会触发事件。违规来源的简化版本如下:

public class ThreadRunner
{
    private bool keepRunning;

    public event EventHandler Started;
    public event EventHandler Finished;

    public void StartThreadTest()
    {
        this.keepRunning = true;
        var thread = new Thread(new ThreadStart(this.LongRunningMethod));
        thread.Start();
    }

    public void FinishThreadTest()
    {
        this.keepRunning = false;
    }

    protected void OnStarted()
    {
        if (this.Started != null)
            this.Started(this, new EventArgs());
    }

    protected void OnFinished()
    {
        if (this.Finished != null)
            this.Finished(this, new EventArgs());
    }

    private void LongRunningMethod()
    {   
        this.OnStarted();

        while (this.keepRunning)
            Thread.Sleep(100);

        this.OnFinished();
    }
}

我接受了测试,检查Finished事件在LongRunningMethod完成后如下所示:

[TestClass]
public class ThreadRunnerTests
{
    [TestMethod]
    public void CheckFinishedEventFiresTest()
    {
        var threadTest = new ThreadRunner();

        bool finished = false;

        object locker = new object();

        threadTest.Finished += delegate(object sender, EventArgs e)
        {
            lock (locker)
            {
                finished = true;
                Monitor.Pulse(locker);
            }
        };

        threadTest.StartThreadTest();
        threadTest.FinishThreadTest();

        lock (locker)
        {
            Monitor.Wait(locker, 1000);
            Assert.IsTrue(finished);
        }
    }
}

所以这里的想法是,在检查是否设置了Finish标志之前,测试将阻塞最多一秒 - 或直到finished事件被触发为止。

显然我做错了,因为有时测试会通过,有时则不会。调试似乎非常困难,而且我期望被击中的断点(例如OnFinished方法)似乎并不总是如此。

我认为这只是我对线程工作方式的误解,所以希望有人可以启发我。

4 个答案:

答案 0 :(得分:15)

锁定在这里不合适,你需要发出一个事件的信号。例如:

    public void CheckFinishedEventFiresTest() {
        var threadTest = new ThreadRunner();
        var finished = new ManualResetEvent(false);
        threadTest.Finished += delegate(object sender, EventArgs e) {
            finished.Set();
        };
        threadTest.StartThreadTest();
        threadTest.FinishThreadTest();
        Assert.IsTrue(finished.WaitOne(1000));
    }

答案 1 :(得分:4)

弗拉德是完全正确的,但我会再次尝试澄清问题:

// This runs on the other thread
threadTest.Finished += delegate(object sender, EventArgs e) {
    // I can't get this lock if the test thread gets here first!
    lock (locker) {
        finished = true;
        Monitor.Pulse(locker);
    }
};

你可以使用某种等待句柄来完成它。我使用ManualResetEvent

ManualResetEvent waitHandle = new ManualResetEvent(false);
threadTest.Finished += delegate(object sender, EventArgs e) {
    finished = true;
    waitHandle.Set();
};

threadTest.StartThreadTest();
threadTest.FinishThreadTest();

// Specify a timeout so your test isn't hostage forever
if (waitHandle.WaitOne(timeout, true)) {
    Assert.IsTrue(finished);
}

答案 2 :(得分:3)

我最近写了一系列关于发布同步和异步事件的对象的单元测试事件序列的博客文章。这些帖子描述了单元测试方法和框架,并提供了完整的源代码和测试。

使用框架测试可以这样编写:

AsyncEventPublisher publisher = new AsyncEventPublisher();

Action test = () =>
{
    publisher.RaiseA();
    publisher.RaiseB();
    publisher.RaiseC();
};

var expectedSequence = new[] { "EventA", "EventB", "EventC" };

EventMonitor.Assert(test, publisher, expectedSequence, TimeoutMS);

EventMonitor执行所有繁重工作并将运行测试(操作)并断言事件以预期的顺序(expectedSequence)引发。它处理异步事件,并在测试失败时输出好的诊断消息。

在帖子中有很多细节描述了问题和方法,以及源代码:

http://gojisoft.com/blog/2010/04/22/event-sequence-unit-testing-part-1/

答案 3 :(得分:2)

你的测试似乎是错误的。假设在threadTest.FinishThreadTest();中的代码获得CheckFinishedEventFiresTest()锁之后。那么测试就会失败。你在这里有一个明确的竞争条件。

请注意,从FinishThreadTest()返回并不保证线程已完成。它只是设置线程的标志,可以随时考虑(没有什么基本上保证线程立即由调度程序运行)。

在您的情况下,线程很可能正忙Sleep()。在调用threadTest.FinishThreadTest();之后,锁定很可能是由执行CheckFinishedEventFiresTest()的线程获取的。显示器将等待1秒然后放弃。锁定将被释放后,代理人只能在那一刻锁定。