This post is fourth in a series about a group TDD experiment to build an application in 5 days using only tests. Read the beginning here.
By day three, our collective knowledge about what we were building was beginning to shape. After reviewing the tests from the previous day, it was time for an interesting discussion: what’s next?
Determining Next Steps
In order to determine our next steps, the team turned the logical flow diagram of our use-case. We decomposed the logical flow into the following steps:
- Receive input from the UI, maybe a menu control or some other UI action.
- Prompt the user for a file, likely using a standard open file dialog.
- Take the response from the dialog and feed it into our stream parser.
- Take the output of the stream parser and build a graph
- Take the graph and update the UI, likely a ViewModel.
Following a Separation of Concerns approach, we want to design our solution so that each part has very little knowledge about the surrounding parts. It was decided that we can clearly separate the building of the graph from the prompting the user part. In my view, we know very little about the UI at this point so we shouldn’t concern ourselves with how the UI initiates this activity. Instead, we can treat the prompting the user for a file and orchestrating interaction with our parser as a single concern.
It was time to split the group in two and start on different parts. Team one would focus on the code that would call the NDependStreamParser; Team two would focus on the code that consumed the list of ProjectAssembly items to produce a graph.
Note: Day Four was spent reviewing and finishing the code for team two. For the purposes of this post, I’m going to focus on the efforts of Team one.
The Next Test
The team decided that we should name this concern, “NewProjectLoader” as it would orchestrate the loading of our model. We knew that we’d be prompting for a file, so we named the test accordingly:
[TestClass] public class NewProjectLoaderTests { [TestMethod] public void WhenLoadingANewProject_WithAValidFile_ShouldLoadModel() { Assert.Fail(); } }
Within a few minutes the team quickly filled in the immediately visible details of the test.
Realization | Code Written |
Following the first example from the day before, the team filled in their assertions and auto-generated the parts they needed.
To make the tests pass, they hard-coded a response. |
Test Code:
var loader = new NewProjectLoader(); IEnumerable<ProjectAssembly> model = loader.Load(); Assert.IsNotNull(model, "Model was not loaded.");Implementation: public IEnumerable<ProjectAssembly> Load() { return new List<ProjectAssembly>(); } |
How should we prompt the user for a file? | Hmmm. |
Our Next Constraint
The team now needed to prompt the user to select a file. Fortunately, WPF provides the OpenFileDialog so we won’t have to roll our own dialog. Unfortunately, if we introduce it into our code we’ll be tightly coupled to the user-interface.
To isolate ourselves from this dependency, we need to introduce a small interface:
namespace DependencyViewer { public interface IFileDialog { string SelectFile(); } }
Through tests, we introduced these changes:
Realization | Code Written |
We need to introduce our File Dialog to our Loader.
We decide our best option is to introduce the dependency through the constructor of the loader. This creates a small compilation error that is quickly resolved. Our tests still passes. |
Test Code:
IFileDialog dialog = null; var loader = new NewProjectLoader(dialog); IEnumerable<ProjectAssembly> model = loader.Load(); Assert.IsNotNull(model, "Model was not loaded.");Implementation: public class NewProjectLoader { private IFileDialog _dialog; public NewProjectLoader( IFileDialog dialog) { _dialog = dialog; } // ... |
Now, how should we prompt for a file?
The code should delegate to our IFileDialog and we can assume that if they select a file, the return value will not be null. The test compiles, but the test fails because the dialog is null. |
Implementation:
public IEnumerable<ProjectAssembly> Load() { string fileName = _dialog.SelectFile(); if (!String.IsNullOrEmpty(fileName)) { return new List<ProjectAssembly>(); } throw new NotImplementedException(); } |
We don’t have an implementation for IFileDialog. So we’ll define a dummy implementation and use Visual Studio to auto-generate the defaults.
Our test fails because the auto-generated code throws an error (NotImplementedException). |
Test Code:
IFileDialog dialog = new MockFileDialog(); var loader = new NewProjectLoader(dialog); IEnumerable<ProjectAssembly> model = loader.Load(); Assert.IsNotNull(model, "Model was not loaded."); |
We can easily fix the test replacing the exception with a non-null file name. | Implementation:
public class MockFileDialog : IFileDialog { public string SelectFile() { return "Foo"; } } |
The test passes, but we’re not done. We need to construct a valid model.
We use a technique known as “Obvious Implementation” and we introduce our NDependStreamParser directly into our Loader. The test breaks again, this time because “Foo” is not a valid filename. |
Implementation:
string fileName = _dialog.SelectFile(); if (!String.IsNullOrEmpty(fileName)) { using(var stream = XmlReader.Create(fileName)) { var parser = new NDependStreamParser(); return parser.Parse(stream); } } //... |
Because our solution is tied to a FileStream, we need to specify a proper file name. To do this we need to modify our MockFileDialog so that we we can assign a FileName from within the test.
In order to get a valid file, we need to include a file as part of the project and then enable Deployment as part of the mstest test settings. (Note: We could have changed the signature of the loader to take a filename, but we chose to keep the dependency to the file here mainly for time concerns.) |
Implementation:
public class MockFileDialog : IFileDialog { public string FileName; public string SelectFile() { return FileName; } } Test Code: [DeploymentItem("AssembliesDependencies.xml")] [TestMethod] public void WhenLoadingANewProject...() { var dialog = new MockFileDialog(); dialog.FileName = "AssembliesDependencies.xml"; var loader = new NewProjectLoader(dialog); //... |
Isolating Further
While our test passes and it represents the functionality we want, we’ve introduced a design problem such that we’re coupled to the implementation details of the NDependStreamParser. Some may make the case that this is the nature of our application, we only need this class and if the parser’s broken so is our loader. I don’t necessarily agree.
The problem with this type of coupling is that when the parser breaks, the unit tests for the loader will also break. If we allow this type of coupling you can draw a logical conclusion that other classes will have tight coupling and thus when the parser breaks it will have a cascade effect on the majority of our tests. This defeats the purpose of our early feedback mechanism. Besides, why design our classes to be black-boxes that will have to change if we introduce different types of parsers?
The solution is to introduce an interface for our parser. Resharper makes this really easy, simply click our class and choose “Extract Interface”.
public interface IGraphDataParser { IEnumerable<ProjectAssembly> Parse(XmlReader reader); }
Adding a Mocking Framework
Whereas we created a hand-rolled mock (aka Test Double) for our IFileDialog, it’s time to introduce a mocking framework that can create mock objects in memory. Using NuGet to simplify our assembly management, we add a reference to Moq to our test project.
Refactoring Steps
We made the following small refactoring changes to decouple ourselves from the NDependStreamParser.
Realization | Code Written |
Stream Parser should be a field.
|
Implementation:
// NewProjectLoader.cs IFileDialog _dialog; NDependStreamParser _parser; public IEnumerable<ProjectAssembly> Load() { string fileName = _dialog.SelectFile(); if (!String.IsNullOrEmpty(fileName)) { using (var stream = XmlReader.Create(fileName)) { _parser = new NDependStreamParser(); return _parser.Parse(stream); } } throw new NotImplementedException(); } |
We need to use the interface rather than the concrete type. | Implementation:
public class NewProjectLoader { IFileDialog _dialog; IGraphDataParser _parser; // ... |
We should initialize the parser in the constructor instead of the Load method. | Implementation:
public class NewProjectLoader { IFileDialog _dialog; IGraphDataParser _parser; public NewProjectLoader(IFileDialog dialog) { _dialog = dialog; _parser = new NDependStreamParser(); } // ... |
We should initialize the parser from outside the constructor.
This introduces a minor compilation problem that requires us to change the test slightly. |
Test Code:
var dialog = new MockFileDialog(); var parser = new NDependStreamParser(); var loader = new NewProjectLoader(dialog, parser); Implementation: public class NewProjectLoader { IFileDialog _dialog; IGraphDataParser _parser; public NewProjectLoader( IFileDialog dialog, IGraphDataParser parser) { _dialog = dialog; _parser = parser; } // ... |
We need to replace our NDependStreamParser with a mock implementation.
|
Test Code:
var dialog = new MockFileDialog(); var paser = new Mock<IGraphDataParser>().Object; var loader = new NewProjectLoader(dialog, parser); |
Strangely enough, there’s a little known feature of Moq that will ensure mocks that return IEnumerable collections will never be null, so our test passes!
Additional Tests
We wrote the following additional tests:
- WhenLoadingANewProject_WithNoFileSpecfied_ShouldNotReturnAModel
Next: Day Four
0 comments:
Post a Comment