Creating Testable Applications Using the MVP Pattern
(The following post talks about ASP.NET, but it actually applies to all UI-based applications, web and non-web. PHP, WPF, Winforms, etc. It does not require a framework, nor anything to install - it's just an interesting way to write your code such that it has a clear separation of concerns).
The Problem
Your typical ASP.NET application is difficult to test, because much of the logic is contained within the codebehind files, which derive from Web.UI.Page, which needs an HttpContext, which is difficult to mock. Furthermore, the output of the methods in the codebehind is often not easily-testable, because it's a side-effect (such as calling DataBind() on a GridView).
This same problem exists in most UI frameworks: How do you test the logic used to generate a UI without having to take heavy dependencies on that UI's implementation (thereby complicating the tests immensely)?
The Goal
Ideally, you'd be able to take all the logic you want to test and put it outside of the UI framework (ie; the ASPX codebehind), such that you can test that logic separately without depending on that UI framework. You want your logic to be able to update the UI, but at the same time need to know as little as possible about the internals of that UI.
Achieving the Separation Using the Model-View-Presenter Pattern
The Model-View-Presenter (MVP) pattern separates your app into three parts:
- The Model: This is the same object model that you currently use for your business objects, data objects, whatever. This remains unchanged.
- The View: This is your ASPX file; but it's as thin and dumb as possible. By that I mean that it only renders data that has been given to it; it doesn't do any thinking about what data it should have and where to get it.
- The Presenter: This is the brains of the operation. The View asks it to act on the user's desires; it consults the model, then tells the view what to render (although not how to render it - that's the key part!).
The key to all this is that the Presenter avoids having a dependency on the View by having all the View's data requirements defined in an interface, and creating the dependency on that interface instead of on the view. That can then easily be mocked up for testing.
All this is best presented as a before/after example. Here's the scenario: We have a page that needs to display a list of people, filtered by a user-defined string. In order to do this, it eventually needs to bind a list of people to a GridView, which will take care of the rendering.
The "Before" Code for handling the user's button press to filter the list:
In PeopleView.aspx.cs:
1: void btnFilter_Click(object sender, EventArgs e)
2: {
3: ShowSomePeople(txtFilter.Text);
4: }
5:
6: private void ShowSomePeople(string filter)
7: {
8: List<Person> lst = LoadAllFromDB().Where(p => p.Name.Contains(filter)).ToList();
9: gvwPeople.DataSource = lst;
10: gvwPeople.DataBind();
11: }
(Here, the LoadallFromDB() call is a call to the Model to get a list of all Person objects from the DB).
The "After" Code:
First we create the interface definition which defines what data our View needs in order to render the page. We know it needs the actual list of people to show, and maybe an error message in case of problems. So, in PeopleInterface.cs:
1: public interface IPeopleInterface
2: {
3: // The list of people to show
4: List<Person> Members { get; set; }
5:
6: // Show this if something went wrong
7: string Error { get; set; }
8: }
In PeopleView.aspx.cs (the codebehind), we implement this interface:
1: public partial class PeopleView : System.Web.UI.Page, IPeopleInterface
2: {
3: // ...
4: }
5:
Then we update ShowSomePeople to call into the Presenter, instead of doing its own thinking:
1: private void ShowSomePeople(string filter)
2: {
3: PeoplePresenter Presenter = new PeoplePresenter(this);
4: Presenter.LoadWithFilter(filter);
5: gvwPeople.DataSource = this.Members;
6: gvwPeople.DataBind();
7: }
Finally the Presenter itself, in PeoplePresenter.cs (a standalone class):
1: public PeoplePresenter(IPeopleInterface v)
2: {
3: View = v; // "View" is a private member
4: }
5:
6: public void LoadWithFilter(string filter)
7: {
8: // Tell the view what data it should be showing
9: View.Members = this.LoadAllFromDB().Where(p => p.Name.Contains(filter)).ToList();
10: }
The interface defines the "contract" between the View and the Presenter, allowing the Presenter to be agnostic of the guts of the View. In theory if you wanted to add a WPF client for this application, you would not need to touch the Model or the Presenter or your unit tests - you would just implement a WPF view that adhered to the same interface.
It may look like much more code, but note that it's just a couple of tiny classes per page; this overhead is fixed and doesn't grow with the complexity of your ASPX page.
Unit Testing
Because you've made your view "dumb" (it mostly just databinds), all your business logic exists in the Presenter and Model, meaning that you probably don't need to test the View. In order to test the Presenter, all you need to do is pass it a mocked up implementation of the interface that it asks for. So:
- Create a mock that implements the interface (example).
- Pass that into a new Presenter.
- Run the Presenter methods.
- Check the properties of the mocked object to see that the Presenter set them correctly.
What about MVC?
As Scott Guthrie has mentioned, MVC is not the new recommended method of building ASP.NET apps; it's just another method. I love the simplicity of the MVC framework, but my team has invested in server controls that are incompatible with it, making it too costly to move to at this point.
MVP gives us similar benefits (testability), without the cost of re-wiring our controls, and with pretty much no learning curve.
Notes:
- Microsoft's Web Client Software Factory gives you templates for project creation which implement this pattern for ASP.NET.
- If you want a comprehensive comparison and history of MVC, MVP and related patterns, see this article.
Avi
Comments
- Anonymous
August 03, 2008
PingBack from http://www.alvinashcraft.com/2008/08/03/dew-drop-august-3-2008/