如何对涉及SynchronizationContext的代码进行单元测试?

4
我有以下代码,我正在尝试使用NUnit和Rhino Mocks进行测试。
void _tracker_IndexChanged(object sender, IndexTrackerChangedEventArgs e)
{
    //  _context is initialised as 
    //  SynchronizationContext _context = SynchronizationContext.Current;
    // _tracker.Index is stubbed to return the value 100
    _context.Post(o => _view.SetTrackbarValue(_tracker.Index), null);
}

在测试用例中,我已经将期望设置为:

_view.Expect(v => v.SetTrackbarValue(100));

当我验证期望时,单元测试会随机失败并显示以下信息:
Test(s) failed. Rhino.Mocks.Exceptions.ExpectationViolationException :
IIndexTrackerView.SetTrackbarValue(100); Expected #1, Actual #0.

我无法识别这里的问题,我该怎么修复它?
2个回答

7
我通常通过封装全局状态的抽象类或接口来解决此类问题,以便可以创建实例并进行模拟。然后,我将我的抽象类或接口的实例注入到使用它的代码中,而不是直接访问全局状态。
这使我能够模拟全局行为,并使我的测试不依赖或涉及该无关行为。
以下是您可以执行此操作的一种方法。
public interface IContext
{
    void Post(SendOrPostCallback d, Object state);
}

public class SynchronizationContextAdapter : IContext
{
    private SynchronizationContext _context;

    public SynchronizationContextAdapter(SynchronizationContext context)
    {
        _context = context;
    }

    public virtual void Post(SendOrPostCallback d, Object state)
    {
        _context.Post(d, state);
    }
}

public class SomeClass
{
    public SomeClass(IContext context)
    {
        _context = context;
    }

    void _tracker_IndexChanged(object sender, IndexTrackerChangedEventArgs e)
    {
        _context.Post(o => _view.SetTrackbarValue(_tracker.Index), null);
    }
    // ...
}

那么,您可以模拟或存根IContext,这样您就不必担心线程问题,并且可以使用一个简单的实现来执行委托。

如果我编写了单元测试以模拟这一点,我还会编写更高级别的“集成”测试,而不是模拟它,但是验证粒度较低。


+1,但真的希望确保调用了 _view.SetTrackbarValue - Prashant Cholachagudda
2
@Prashant:那我肯定会对“context”类进行存根而不是模拟。让它直接调用传递给它的委托。在您的单元测试中,将您的期望设置在视图上,而不是在上下文类上。将此测试与一些简单的基于UI的集成测试相结合,以确保在替换实际的“SynchronizationContext”实现时不会出现任何奇怪的错误。 - Merlyn Morgan-Graham

1

我已经有一段时间没有使用Rhino Mocks了,所以我无法记住确切的语法。但是如果像Merlyn建议的那样重构不是一个选项,那么另一个解决方案是使用ManualResetEvent等待您的Mock对象被处理。

大概像这样:

[Test]
public void ATest(){
  ManualResetEvent completed = new ManualResetEvent(false);

  _view.Expect(v => v.SetTrackbarValue(100)).Do(() => completed.Set());
  //Stuff done here...   
  Assert.IsTrue(completed.WaitOne(1000), "Waited a second for a call that never arrived!");

}

这样,您可以等待直到另一个线程触发该事件,然后您可以继续执行。确保设置合理的超时时间,以免无限期等待!


你为什么认为会是另一个线程更新视图?我认为会是同一个线程,在WaitOne语句上暂停。 - Snowbear
@Snowbear - 如果调用SynchronisationContext上的post方法的线程与运行测试的线程相同,则此方法显然不起作用。我假设使用SynchronisationContext来做出这个假设,即它正在运行在一个单独的线程上。 - Russell Troywest

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