从线程中触发事件的单元测试

7

我在对一个触发线程开始和结束事件的类进行单元测试时遇到了问题。以下是有问题的源码简化版本:

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

然后我有一个测试来检查LongRunningMethod完成后是否会触发Finished事件,如下所示:

[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方法)并不总是出现。

我假设这只是我对线程工作方式的误解,希望有人能启发我。


感谢所有的回复。我还通过让线程工作方法(即LongRunningMethod)在启动时设置自己的控制标志引入了另一个错误。今天确实是如何在代码中引入竞争条件的速成课程,哎! - Dougc
4个回答

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

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

谢谢Jeff。似乎仍然存在一个奇怪的问题,即线程有时在FinishThreadTest方法切换keepRunning标志后不会退出?即使我将超时时间增加到非常高的值。 - Dougc
LongRunningMethod不存在?也许第二个WaitOne参数(退出上下文)有些问题。我会尝试nobugz的示例,它旨在完成相同的事情,但无论如何都更好(省去了多余的bool)。 - Jeff Sternal
没关系,我当时有点傻(请看我在原问题中的评论)。谢谢! - Dougc

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)承担了所有重活,它将运行测试(action),并断言事件按预期序列(expectedSequence)被触发。它处理异步事件,并在测试失败时打印出漂亮的诊断消息。
这些帖子中描述了许多问题和方法,还包括源代码。

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


2
您的测试似乎有误。假设在threadTest.FinishThreadTest();之后,CheckFinishedEventFiresTest()中的代码获得了锁。那么测试将会失败。您在这里明显存在竞争条件。
请注意,从FinishThreadTest()返回并不保证线程已经完成。它只是为线程设置了标志,该标志可以在任何时刻被考虑(基本上没有什么保证线程会立即由调度程序运行)。
在您的情况下,线程很可能正在忙于Sleep()。在调用threadTest.FinishThreadTest();之后,锁很可能会被执行CheckFinishedEventFiresTest()的线程获取。监视器将等待1秒钟,然后放弃。在此之后,锁将被释放,因此委托将只能在那个时刻锁定。

啊,现在明白了。谢谢 Vlad。 - Dougc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接