Recently, I've begun to think about the overall process of designing and building software using TDD. As I've focused on abstracting each layer for better testing, the benefits of Outside-In development has become more and more apparent. So here I'd like to layout how I've begun to go about the work of designing and building using this approach. I do not pretend that this approach is by any means novel, but I do hope it will be explained well enough here to be of benefit to others.
Starting from the UI (the Outside)
Suppose you have a typical business application that has a user interface (UI) and some backend database. Assuming you have done the work to define what the application is supposed to do, you may very well have a number, if not all, the UIs defined. I typically do this in a functional spec with collaboration with (or at least sign off from) the business unit. Chances are, you will use a fairly familiar architecture and technology stack. I currently use .NET, Entity Framework and the MVP pattern. That coupled with best of practices for software architecture gives us the layers we need. Starting from the UI, they are:
- UI (or View)
- Presentation
- Service
- Data Access
Most UIs can be broken down into data elements and actions. The MVP pattern separates these nicely between a View and a Presenter. When I look at a mock-up of a UI, I can start to see what those data elements and actions are. Thus, my design work starts by defining the View and Presenter interfaces.
Suppose we have a UI that looks like this:
public interface GroceryListViewInterface
{
List<Grocery> GroceryList {get;set;}
Grocery SelectedGrocery {get;}
}
I'll also need to start defining what a Grocery is:
public class Grocery
{
public int? Id {get;set;}
public string Name {get;set;}
}
The actions that a presenter must perform are fairly evident as well:
public interface GroceryListPresenterInterface
{
Load(); // I've got to start somewhere
New(); // that's a toolbar option
Close(); // another toolbar option
Edit(); // this is a link in the grid
Delete(); // again, a link in the grid
ConfirmDelete(); // see below
}
Designing through Testing
Were I creating UML sequence diagrams in a tool like Visio, I would next consider what I must do in order to perform the functions in the presenter. It's no different here. But in this case, so that I have something useful that validates my code, I will design the service layer by building the unit tests for the presenter that I have just defined. This will require that I create the presenter class and provide default implementations for the methods (they all will throw the NotImplementedException) and I will start an interface for the service.
In the unit test for the presenter class, I will use a mocking framework to define the service. I use Moq, but there are others out there that would work as well. The advantages of a mocking framework is that you do not need to implement the class you are mocking; you merely need the interface. I will create a single test fixture for a class and then stub out the tests that I need. For brevity, I've only included a few in the following example:
[TestClass]
public class GroceryList_TestFixture
{
private Mock<GroceryListServiceInterface> serviceMock;
private Mock<GroceryListViewInterface> viewMock;
#region Load Tests
[TestMethod]
public void Load_Should_Load_Existing_Groceries()
{
throw new NotImplementedException();
}
#endregion
#region New Tests
[TestMethod]
public void New_Should_Add_A_Blank_Grocery()
{
throw new NotImplementedException();
}
#endregion
}
Notice the long names for the tests. That makes them easy to read, especially with an underscore. I also like to express them using an subjunctive voice, i.e. I say "should" instead of "will" or "can". Another thing to note is the use of regions. I only showed a single test for each method here, and that may be all that's required. However, when there are many scenarios, I like them well organized. Finally, since I haven't implemented any of the tests, they all throw an exception. This will ensure that the tests fail if I happen to run them. That frees me to stub out as many tests as I need without worrying that I might forget to implement them.
When defining these tests, I want to consider all the various scenarios that might occur in a method. Lets consider the ConfirmDelete() method. Whenever I delete something, I want to confirm that operation with the user. "Delete" to me should always ask the question of the user, "Do you really want to do this?" Since I cannot assume my presenter can open a modal dialog, I always have a follow up method with a name like "ConfirmDelete" to be used if the user confirms the action.
Thinking about what happens during the ConfirmDelete() method, where the real work to delete the Grocery is done, a couple scenarios occur to me:
- When I confirm the deletion of a Grocery, the Grocery should be deleted.
- If an error occurs while confirming the deletion of a Grocery, I should see an error message.
[TestMethod]
public void ConfirmDelete_Should_Delete_A_Grocery_When_No_Errors_Occur()
{
throw new NotImplementedException();
}
[TestMethod]
public void ConfirmDelete_Should_Show_A_Warning_When_An_Exception_Occurs()
{
throw new NotImplementedException();
}
The next part of this step is where the real design work is done. Let's take the first test and work through it.
I'll first divide the test method into the three typical parts of a test. Different people have different names for these parts, but they are basically the same and fairly self explanatory:
[TestMethod]
public void ConfirmDelete_Should_Delete_A_Grocery_When_No_Errors_Occur()
{
// setup
// execute
// validate
}
I setup the test by initializing the object I am going to test and by setting up the mocks. I know I'll need to instantiate the mocks and the presenter object for each test, and that effort is the same, so I'll go ahead and create a private method to do that. I like to return the interface instead of the actual object.
private GroceryListPresenterInterface SetupPresenter()
{
serviceMock = new Mock<GroceryListServiceInterface>(MockBehavior.Strict)
{ CallBase = true };
viewMock = new Mock<GroceryListViewInterface>(MockBehavior.Struct)
{ CallBase = true };
return new GroceryListPresenter(
serviceMock.Object, viewMock.Object);
}
You can see that the implementation of the presenter takes the service and the view as parameters on the constructor. I could have done this differently, but this is a simple approach that guarantees that the presenter will get its dependencies.
Now that the presenter object is ready and the mocks have been instantiated, I can proceed with the setup portion of the test, which is ultimately the design of the method.
This method does not require much. All I need to do is get the selected Grocery and then pass it to the service through a delete method. So, I'll setup the mock objects to express this:
...
// setup
IGroceryListPresenterInterface presenter = SetupPresenter();
viewMock.SetupGet( v => v.SelectedGrocery).Returns(new Grocery());
serviceMock.Setup( s => s.Delete(It.IsAny<Grocery>());
...
See the Moq documentation for details on how the mock objects work.
That's about it. All I need to do now is execute the presenter method and validate the mocks:
// execute
presenter.ConfirmDelete();
// validate
viewMock.VerifyAll();
serviceMock.VerifyAll();
The next step will be to implement the test for the exception scenario. In that case, I would have the serviceMock throw an exception and express how the it would be shown in the view, through some other method or property on the view. That means, of course, I will amend my view interface. That's the very kind of detail I want to discuss in writing these tests!
Once the tests for all the scenarios have been implemented - and, of course, they all fail - I will have accomplished an Outside-In approach to designing and developing using TDD principals. The service layer is now succinctly defined for this particular feature. I have the methods I need and no more. Even the domain objects have been designed without anything extra.
Building from the Outside-In
The next step is to implement the presenter itself. I can do this before I even consider the design of the service methods because of the tests I have written. I expect them all to eventually pass before I proceed with the service design. I might even move on to other UIs before designing the service. This is a significant part of the Outside-In approach. By doing this, I can discover flaws in my designs before I've gone very far at all. Since the service layer is just an interface, it is really quite easy to adjust. That holds true for the domain objects, as well.
After the presenter has been implemented and all tests have passed, I can proceed in the same manner with designing and building the service layer. After that, the data access layer is next. At that point I'll need to deal with how the domain objects will be persisted and queried. It is noteworthy that until this point, I will not have built a database at all. In fact, using the Code First features of Entity Framework (see Code First Development with Entity Frameworkf 4) I am able to complete the data access layer without really building the database (you can have it automatically generated into SQLCE or MSDE, but that's a topic for another post.)
There are many advantages to this approach for designing and building software:
- By starting with the requirements (the UI in this case) I start with the customer's perspective and build only what is necessary to fulfill those requirements.
- Missing functionality becomes more evident early in the process. In fact, functionality in the back-end is far less likely to be missed because I will not have defined what the backend looks like until I've completed the tests for the front end.
- Using unit tests as a low-level design tool provides me with a complete set of unit tests throughout the development process. Continuous refactoring is part of the TDD process; the unit tests make sure my code continues to work as I refactor.
No comments:
Post a Comment