代码之家  ›  专栏  ›  技术社区  ›  bendewey

支持多线程的TDD测试重构

  •  7
  • bendewey  · 技术社区  · 16 年前

    所以我是TDD的新手,我用MVP模式成功地创建了一个不错的小样本应用程序。我当前解决方案的主要问题是它阻塞了UI线程,因此我试图设置演示者使用SynchronizationContext.current,但当我运行测试时,SynchronizationContext.current为空。

    线程前演示者

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

    现在,在向演示者添加了SynchronizationContext线程之后,我尝试在我的假视图上为状态消息设置AutoResetEvent,但在运行测试时,SynchronizationContext.current为空。我意识到我在新演示者中使用的线程模型并不完美,但这是测试多线程的正确技术吗?为什么我的SynchronizationContext.current为空?我该怎么办?

    线程后的演示者

    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();
            }
        }
        ...
    }
    
    2 回复  |  直到 16 年前
        1
  •  3
  •   tvanfosson    16 年前

    我在ASP.NET MVC中遇到了类似的问题,其中缺少的是httpContext。您可以做的一件事是提供一个备用的构造函数,它允许您注入一个模拟的同步上下文或公开一个执行相同操作的公共setter。如果不能在内部更改SynchronizationContext,则在默认构造函数中创建一个设置为SynchronizationContext.current的属性,并在代码中使用该属性。在备用构造函数中,您可以将模拟上下文分配给属性——或者,如果给它一个公共setter,您也可以直接分配给它。

    公共类ftppresenter:iftpPresenter { 公共同步上下文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();
    
        ...
       }
    

    我将要做的另一个观察是,我可能会让您的演示者依赖于线程类的接口,而不是直接依赖于线程。我不认为单元测试应该创建新的线程,而应该与一个模拟类交互,该类只确保调用创建线程的正确方法。你也可以注入这种依赖。

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

        2
  •  1
  •   Benzen    16 年前

    你的演示者需要很多应用程序逻辑。我将在一个具体的模型中隐藏上下文和线程,并单独测试功能。