This post is eighth in a series about a group TDD experiment to build an application in
57 days using only tests. Read the beginning here.
Today is the day we test the untestable. Early in the experiment we hit a small road block when our code needed to interact with the user-interface. Since unit-testing the user-interface wasn’t something we wanted to pursue, we wrapped the OpenFileDialog behind a wrapper with an expectation that we would return to build out that component with tests later. The session for this day would prove to be an interesting challenge.
The Challenges of Physical Dependencies
Although using a wrapper to shield our code from difficult to test dependencies is a common and well accepted technique, it would be irresponsible not to test the internal implementation details of that wrapper. Testing against physical dependencies is hard because they introduce a massive amount of overhead, but if we can isolate the logic from the physical dependency we can use unit tests to get 80-90% of the way there. To get the remaining part, you either test manually or write a set of integration or functional tests.
The technique outlined below can be used for testing user-interface components like this one, email components and in a pinch it can work for network related services.
Testing our Wrapper
Time to write some tests for our IFileDialog. I have some good news and bad news.
The good news is Microsoft provides a common OpenFileDialog as part of WPF, meaning that I don’t have to roll my own and I can achieve a common look and feel with other applications with little effort. This also means we can assume that the FileOpenDialog is defect free, so we don’t have to write unit tests for it.
The bad news is, I use this common so infrequently that I forget how to use it.
So instead of writing a small utility application to play with the component, I write a test that shows me exactly how the component works:
[TestMethod] public void WhenSelectAFileFromTheDialog_AndUserSelectsAFile_ShouldReturnFileName() { var dialog = new OpenFileDialog(); dialog.Show(); // this will show the dialog Assert.IsNotNull(dialog.FileName); }
When I run this test, the file dialog is displayed. If I don’t select a file, the test fails. Now that we know how it works, we can rewrite our test and move this code into a concrete implementation.
Unit Test:
[TestMethod] public void WhenSelectingAFile_AndUserMakesAValidSelection_ShouldReturnFileName() { var subject = new FileDialog(); string fileName = subject.SelectFile(); Assert.IsNotNull(fileName); }
Production Code:
public class FileDialog : IFileDialog { public string SelectFile() { var dialog = new OpenFileDialog(); dialog.Show(); return dialog.FileName; } }
The implementation is functionally correct, but when I run the test I have to select a file in order to have the test pass. This is not ideal. We need a means to intercept the dialog and simulate the user selecting a file. Otherwise, someone will have to babysit the build and manually click file dialog prompts until the early morning hours.
Partial Mocks To the Rescue
Instead of isolating the instance of our OpenFileDialog with a mock implementation, we intercept the activity and allow ourselves to supply a different implementation for our test. The following shows a simple change to the code to make this possible.
public class FileDialog : IFileDialog { public string SelectFile() { var dialog = new OpenFileDialog(); Show(dialog); return dialog.FileName; } internal virtual void Show(OpenFileDialog dialog) { dialog.Show(); } }
This next part is a bit weird. In the last several posts, we’ve used Moq to replace our dependencies with fake stand-in implementations. For this post, we’re going to mock the subject of the test, and fake out specific methods on the subject. Go back and re-read that. You can stop re-reading that now.
As an aside: I often don’t like showing this technique because I’ve seen it get abused. I’ve seen abuses where developers use this technique to avoid breaking classes down into smaller responsibilities; they fill their classes with virtual methods and then stub out huge chunks of the subject. This feels like shoddy craftsmanship and doesn’t sit well with me – granted, it works, but it leads to problems. First, the areas that they’re subverting never get tested. Secondly, it’s too easy for developers to forget what they’re doing and they start to write tests for the mocking framework instead of the subject’s functionality. So use with care. In this example, I’m subverting one line of a well tested third-party component in order to avoid human-involvement in the test.
In order to intercept the Show method and replace it with our own implementation we can use Moq’s Callback feature. I’ve written about this Moq’s support for Callbacks before, but in a nutshell Moq can intercept the original method and inbound arguments for use within your test.
Our test now looks like this:
[TestMethod] public void WhenSelectingAFile_AndUserMakesAValidSelection_ShouldReturnFileName() { // setup a partial mock for our subject var mock = new Mock<FileDialog>(); FileDialog subject = mock.Object; // The Show method in our FileDialog is virtual, so we can setup // an alternate behavior when it's called. // We configure the Show method to call the SelectAFile method // with the original arguments mock.Setup( partialMock => partialMock.Show(It.IsAny<OpenFileDialog>()) .Callback( SelectAFile ); string fileName = subject.SelectFile(); Assert.IsNotNull(fileName); } // we alter the original inbound argument to simulate // the user selecting a file private void SelectAFile(OpenFileDialog dialog) { dialog.FileName = "Foo"; }
Now when our test runs the FileDialog returns “Foo” without launching a popup. Now we can write tests for a few extra scenarios:
[TestMethod] public void WhenSelectingAFile_AndTheUserCancelsTheFileDialog_NoFileNameShouldBeReturned() { // we're mocking out the call to show the dialog, // so without any setup on the mock, the dialog will not return a value. string fileName = Subject.SelectFile(); Assert.IsNull(fileName); } [TestMethod] public void WhenSelectingAFile_BeforeUserSelectsAFile_EnsureDefaultDirectoryIsApplicationRootFolder() { // ARRANGE: string expectedDirectory = Environment.CurrentDirectory; Mock.Get(Subject) .Setup(d => d.ShowDialog(It.IsAny<OpenFileDialog>())) .Callback<OpenFileDialog>( win32Dialog => { // ASSERT: validate that the directory is set when the dialog is shown Assert.AreEqual(expectedDirectory, win32Dialog.InitialDirectory); }); // ACT: invoke showing the dialog Subject.SelectFile(); }
Next: Review some of the application metrics for our experiment in the Guided by Tests – Wrap Up.
No comments:
Post a Comment