TDD测试重构以支持多线程

7

我是一名TDD新手,成功地使用MVP模式创建了一个漂亮的小样本应用程序。目前解决方案的主要问题是它阻塞了UI线程。因此,我尝试设置Presenter以使用SynchronizationContext.Current,但当我运行测试时,SynchronizationContext.Current为null。

线程之前的Presenter

public class FtpPresenter : IFtpPresenter
{
    ...
    void _view_GetFilesClicked(object sender, EventArgs e)
    {
        _view.StatusMessage = Messages.Loading;

        try
        {
            var settings = new FtpAuthenticationSettings()
            {
                Site = _view.FtpSite,
                Username = _view.FtpUsername,
                Password = _view.FtpPassword
            };
            var files = _ftpService.GetFiles(settings);

            _view.FilesDataSource = files;
            _view.StatusMessage = Messages.Done;        
        }
        catch (Exception ex)
        {
            _view.StatusMessage = ex.Message;
        }
    }
    ...
}

线程前测试

[TestMethod]
public void Can_Get_Files()
{
    var view = new FakeFtpView();
    var presenter = new FtpPresenter(view, new FakeFtpService(), new FakeFileValidator());

    view.GetFiles();
    Assert.AreEqual(Messages.Done, view.StatusMessage);
}

现在,在我为Presenter添加了SynchronizationContext Threading之后,我尝试在我的Fake View上设置一个AutoResetEvent来获取StatusMessage,但是当我运行测试时,SynchronizationContext.Current为空。我意识到我在新的Presenter中使用的线程模型并不完美,但是这是测试多线程的正确技术吗?为什么SynchronizationContext.Current为空?我应该做些什么呢?

Presenter添加Threading后

public class FtpPresenter : IFtpPresenter
{
    ...
    void _view_GetFilesClicked(object sender, EventArgs e)
    {
        _view.StatusMessage = Messages.Loading;

        try
        {
            var settings = new FtpAuthenticationSettings()
            {
                Site = _view.FtpSite,
                Username = _view.FtpUsername,
                Password = _view.FtpPassword
            };
            // Wrap the GetFiles in a ThreadStart
            var syncContext = SynchronizationContext.Current;
            new Thread(new ThreadStart(delegate
            {
                var files = _ftpService.GetFiles(settings);
                syncContext.Send(delegate
                {
                    _view.FilesDataSource = files;
                    _view.StatusMessage = Messages.Done;
                }, null);
            })).Start();
        }
        catch (Exception ex)
        {
            _view.StatusMessage = ex.Message;
        }
    }
    ...
}

多线程测试

[TestMethod]
public void Can_Get_Files()
{
    var view = new FakeFtpView();
    var presenter = new FtpPresenter(view, new FakeFtpService(), new FakeFileValidator());

    view.GetFiles();
    view.GetFilesWait.WaitOne();
    Assert.AreEqual(Messages.Done, view.StatusMessage);
}

虚假浏览量

public class FakeFtpView : IFtpView
{
    ...
    public AutoResetEvent GetFilesWait = new AutoResetEvent(false);
    public event EventHandler GetFilesClicked = delegate { };
    public void GetFiles()
    {
        GetFilesClicked(this, EventArgs.Empty);
    }
    ...
    private List<string> _statusHistory = new List<string>();
    public List<string> StatusMessageHistory
    {
        get { return _statusHistory; }
    }
    public string StatusMessage
    {
        get
        {
            return _statusHistory.LastOrDefault();
        }
        set
        {
            _statusHistory.Add(value);
            if (value != Messages.Loading)
                GetFilesWait.Set();
        }
    }
    ...
}

好问题!我正在尝试解决类似的问题! - Alex Kofman
2个回答

3

我在使用ASP.NET MVC时也遇到了类似的问题,即缺少HttpContext。你可以提供一个替代构造函数,允许注入模拟的SynchronizationContext或公开一个可执行相同操作的公共setter。如果无法在内部更改SynchronizationContext,则创建一个属性,在默认构造函数中将其设置为SynchronizationContext.Current,并在代码中始终使用该属性。在备用构造函数中,您可以将模拟上下文分配给该属性 - 或者如果您给它一个公共setter,则可以直接将其赋值给它。

public class FtpPresenter : IFtpPresenter { public SynchronizationContext CurrentContext { get; set; }

   public FtpPresenter() : this(null) { }

   public FtpPresenter( SynchronizationContext context )
   {
       this.CurrentContext = context ?? SynchronizationContext.Current;
   }

   void _view_GetFilesClicked(object sender, EventArgs e)
   {
     ....
     new Thread(new ThreadStart(delegate
        {
            var files = _ftpService.GetFiles(settings);
            this.CurrentContext.Send(delegate
            {
                _view.FilesDataSource = files;
                _view.StatusMessage = Messages.Done;
            }, null);
        })).Start();

    ...
   }

我想提出另一个观察结果,即我认为您的演示文稿应该依赖于Thread类的接口而不是直接依赖于Thread。我认为您的单元测试不应该创建新线程,而应该与一个模拟类交互,该模拟类只确保调用了创建线程的正确方法。您还可以注入该依赖项。

如果在调用构造函数时SynchronizationContext.Current不存在,则可能需要将分配逻辑移到getter中并进行延迟加载。


在我的测试中,我应该使用什么来替换SynchronizationContext.Current?你有任何代码示例吗? - bendewey
我不知道Synchronizationcontext是否符合某个接口。如果是这样,您可以模拟一个使用相同接口的类,并注入该接口。如果不是,则可以定义一个包装SynchronizationContext的类来实现(自己的)接口,并模拟包装类。 - tvanfosson
查看HttpContextWrapper的源代码(这是我熟悉的内容),在www.codeplex.com/aspnet的MVC源代码树中寻找实现此操作的想法。 - tvanfosson
跟进一下,我喜欢创建一个公共的SynchronizationContext SyncContext { get; set; }的想法。 - bendewey
1
更好的做法是我添加了 [TestInitialize()] public void Initialize() { SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); } - bendewey
显示剩余3条评论

1

你的Presenter里有太多的应用逻辑。我会把上下文和线程隐藏在一个具体的模型中,单独测试功能。


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