2011-12-02 136 views
4

我有下面的代码,我试图用NUnit和Rhino mocks来测试。如何测试涉及SynchronizationContext的代码?

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

,当我确认期望,单元测试与消息randomaly失败

Test(s) failed. Rhino.Mocks.Exceptions.ExpectationViolationException : 
IIndexTrackerView.SetTrackbarValue(100); Expected #1, Actual #0. 

我没有在这里找到问题,如何我是否修复它?

回答

6

我通常通过使用抽象类或接口封装全局状态来解决像这样的问题,抽象类或接口可以被实例化和嘲弄。然后,我不是直接访问全局状态,而是将我的抽象类或接口的实例注入到使用它的代码中。

这让我嘲笑全球的行为,并使其成为我的测试不依赖于或行使不相关的行为。

以下是您可以这样做的一种方法。

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,所以你不必担心线程,并且可以使用一个简单的实现,只是执行的委托。如果我编写了单元测试来嘲笑这一点,我还会编写更高级别的“集成”测试,但是它没有模拟出它,但是没有更细化的验证。

+0

+1,但确实要确保调用了_view.SetTrackbarValue! –

+2

@Prashant:那么我肯定会把“上下文”类剔除,而不是嘲笑它。让它直接调用传递给它的委托。在你的单元测试中,在视图上而不是在上下文类中设置你的期望值。将此测试与几个简单的基于UI的集成测试相结合,以确保在替换真正的SynchronizationContext实现时不会出现任何奇怪的错误。 –

1

我有一段时间没有使用Rhino Mocks,所以我不记得确切的语法,但如果Merlyn建议的重构不是一个选项,那么另一种解决方案是使用ManualResetEvent等待,直到您的模拟已经采取行动。

事情是这样的:

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

} 

这样,你可以等待,直到其他线程触发事件,此时你可以继续。确保有一个明智的超时,所以你不要永远等待!

+0

为什么你认为这将是更新视图的另一个线程?我认为这将是'WaitOne'语句中停止的同一个线程。 – Snowbear

+0

@Snowbear - 如果SynchronisationContext上的线程调用帖子是运行测试的线程,那么这种方法显然无法工作。我正在使用一个SynchronisationContext来做一个假设,它正在一个单独的线程上运行。 –