views:

405

answers:

2

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();
        }
    }
    ...
}
+3  A: 

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.

tvanfosson
What would I use in my test in replace of the SynchronizationContext.Current? Do you have any code samples?
bendewey
I don't know if Synchronizationcontext conforms to an interface. If so, you could mock up a class that uses the same interface and inject the interface instead. If not, you can define a wrapper around SynchronizationContext that does implement (your own) interface and mock up the wrapper class.
tvanfosson
Look at the source for HttpContextWrapper (which is what I'm familiar with) at www.codeplex.com/aspnet in the MVC source tree for ideas on how to do this.
tvanfosson
To follow up i like the idea of creating a public SynchronizationContext SyncContext { get; set; }
bendewey
Then in my constructor after my other inits I have a SyncContext = SynchronizationContext.Current; So in my tests I will create a FakeContext, I'm looking at the MVC Codeplex stuff now.
bendewey
thanks for all your help, in my test I just set the syncContext Property to new SynchronizationContext(); Although after all that a found a different and maybe better solution.i didn't realize that there was a SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
bendewey
I added that SetSynchroContext and in my test and didn't have to add that property or add the new constructor.
bendewey
Better yet I added [TestInitialize()]public void Initialize(){ SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());}
bendewey
Glad to be of help.
tvanfosson
+1  A: 

You have to much app-logic in your presenter. I would hide contexts and threads inside a concrete model and test the functionality alone.