Using Hampers Testing: Enter the Factory

I’ve been writing a lot of C# lately, and paying a lot of attention to my test coverage while I’m at it. Everything was going great until I wanted to use the FolderDialogBrowser to let my users select a directory.

FolderDialogBrowser implements IDisposable, so I naturally reached for a Using block.

using (var folderPicker = new FolderBrowserDialog)
{
    if (folderPicker.ShowDialog() != DialogResult.OK)
    {
        return;
    }

    //...
}

Then I stopped dead. I can’t do that. This will display a GUI and any hope of running automated tests against this method is lost.

I know! I’ll create a mock and inject it!

No dice. There’s no interface to mock. The class is a concrete implementation, like most everything else in the System.Windows.Forms namespace. This isn’t going to work either.

So, I define my own interface and create a wrapper class that implements it.

public interface IFolderBrowser : IDisposable
{
    string Description { get; set; }
    bool ShowNewFolderButton { get; set; }
    Environment.SpecialFolder RootFolder { get; set; }
    string SelectedPath { get; set; }
    DialogResult ShowDialog();
}

public class FolderBrowser : IFolderBrowser
{
    private readonly FolderBrowserDialog _dialog;

    public FolderBrowser(string description, bool showNewFolderButton, Environment.SpecialFolder rootFolder)
    {
        _dialog = new FolderBrowserDialog
        {
            Description = description,
            RootFolder = rootFolder,
            ShowNewFolderButton = showNewFolderButton
        };
    }

    public FolderBrowser(string description, bool showNewFolderButton)
        :this(description, showNewFolderButton, Environment.SpecialFolder.MyDocuments)
    { }

    public FolderBrowser(string description)
        : this(description, true)
    { }

    public string Description
    {
        get { return _dialog.Description; }
        set { _dialog.Description = value; }
    }

    public bool ShowNewFolderButton
    {
        get { return _dialog.ShowNewFolderButton; }
        set { _dialog.ShowNewFolderButton = value; }
    }

    public Environment.SpecialFolder RootFolder
    {
        get { return _dialog.RootFolder; }
        set { _dialog.RootFolder = value; }
    }

    public string SelectedPath
    {
        get { return _dialog.SelectedPath; }
        set { _dialog.SelectedPath = value; }
    }

    public DialogResult ShowDialog()
    {
        return _dialog.ShowDialog();
    }

    public void Dispose()
    {
        if (_dialog != null)
        {
            _dialog.Dispose();
        }
    }
}

Awesome! So now I just inject this bad boy in!

class Foo
{
    IFolderBrowser _folderBrowser;
    Foo(IFolderBrowser folderBrowser)
    {
        _folderBrowser = folderBrowser;
    }

    void Bar()
    {
        if (_folderBrowser.ShowDialog() != DialogResult.OK)
        {
            return;
        }

        //...
    }

Hmm… I’ve solved my original problem. I can inject a mock, but now I’ve lost the benefit of utilizing the Using block. I could just dispose of it when I’m done, but that’s a big flaming side effect just begging to become a bug. I could make Foo implement disposable, or just move the Using block up a level to where I’m injecting it, but that’s really just moving the problem. No. I need to solve it. There must be some sort of known design pattern for this, but which one?

At this point I’m stumped, so I do what all good developers do. I bounced the problem off a friend of mine. What he had to say made me feel down right foolish. The answer was staring me in the face. “Use a factory.” A factory! Of course! I use them all the time in my VBA development. (COM loves class factories after all.) I had just never considered that it would be the perfect solution for this problem, because I had never seen it used this way before.

Finally, I whipped up this simple interface and factory class.

public interface IFolderBrowserFactory
{
    IFolderBrowser CreateFolderBrowser(string description);

    IFolderBrowser CreateFolderBrowser(string description, bool showNewFolderButton);

    IFolderBrowser CreateFolderBrowser(string description, bool showNewFolderButton, 
        Environment.SpecialFolder rootFolder);
}

public class DialogFactory : IFolderBrowserFactory
{
    public IFolderBrowser CreateFolderBrowser(string description)
    {
        return new FolderBrowser(description);
    }

    public IFolderBrowser CreateFolderBrowser(string description, bool showNewFolderButton)
    {
        return new FolderBrowser(description, showNewFolderButton);
    }

    public IFolderBrowser CreateFolderBrowser(string description, bool showNewFolderButton, Environment.SpecialFolder rootFolder)
    {
        return new FolderBrowser(description, showNewFolderButton, rootFolder);
    }
}

And injected it in instead of my wrapper.

class Foo
{
    IFolderBrowserFactory _folderBrowserFactory;
    Foo(IFolderBrowserFactory folderBrowserFactory)
    {
        _folderBrowserFactory = folderBrowserFactory;
    }

    void Bar()
    {
        using (var folderBrowser = _folderBrowserFactory.CreateFolderBrowser("Pick A Folder"))
        {
            if (folderBrowser.ShowDialog() != DialogResult.OK)
            {
                return;
            }

            //...
        }
    }
}

Now, I can easily mock up both the dialog and a factory that returns the mock version of the dialog. (I use Moq for my mocking framework.)

var folderBrowser = new Mock<IFolderBrowser>();
var factory = new Mock<IFolderBrowserFactory>();
factory.Setup(f => f.CreateFolderBrowser(It.IsAny<string>())).Returns(folderBrowser.Object);

Beautiful, I just added another trick to my bag and maybe you’ve just added one to yours too.

Until next time,
Semper Cogitet

I recently found a potentially cleaner way to accomplish the factory pattern with delegates instead. It may be of interest dear reader.

Advertisements

, , , , , , , , , , ,

  1. Leave a comment

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: