Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TDD Test Refactoring to support MultiThreading

So I'm a newbie to TDD, and I successfully created a nice little sample app using the MVP pattern. The major problem to my current solution is that its blocking the UI thread, So I was trying to setup the Presenter to use the SynchronizationContext.Current, but when I run my tests the SynchronizationContext.Current is null.

Presenter Before 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
            };
            var files = _ftpService.GetFiles(settings);

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

Test Before Threading

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

Now after I added a SynchronizationContext Threading to the Presenter I tried to set a AutoResetEvent on my Fake View for the StatusMessage, but when I run the test the SynchronizationContext.Current is null. I realize that the threading model I'm using in my new Presenter isn't perfect, but is this the right technique for Testing Multithreading? Why is my SynchronizationContext.Current null? What should I do instead?

Presenter After 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;
        }
    }
    ...
}

Test after threading

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

Fake View

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();
        }
    }
    ...
}
like image 290
bendewey Avatar asked Dec 11 '08 20:12

bendewey


1 Answers

I've run into similar problems with ASP.NET MVC where it is the HttpContext that is missing. One thing you can do is provide an alternate constructor that allows you to inject a mock SynchronizationContext or expose a public setter that does the same thing. If you can't change the SynchronizationContext internally, then make a property that you set to the SynchronizationContext.Current in the default constructor and use that property throughout your code. In your alternate constructor, you can assign the mock context to the property -- or you can assign to it directly if you give it a public 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();

    ...
   }

One other observation that I would make is that I would probably have your presenter depend on an interface to the Thread class rather than on Thread directly. I don't think that your unit tests should be creating new threads but rather interacting with a mock class that just ensures that the proper methods to create threads get called. You could inject that dependency as well.

If the SynchronizationContext.Current doesn't exist when the constructor is called, you may need to move the assignment logic to Current into the getter and do lazy load.

like image 187
tvanfosson Avatar answered Sep 27 '22 18:09

tvanfosson