Background
One of the patterns we are using in all of our current projects at our company is the Model-View-Presenter pattern (which I'll refer to as the MVP pattern from this point forward). This is a great way of breaking out presentation logic into a highly testable and reusable layer of the application.
Most of our implementations of the MVP pattern make use of DTOs (Data Transfer Objects) as the mechanism to pass data to and from the service layer and the presentation layer. This allows us to make a clear separation between our presentation layer and domain model which lives behind the services layer. But enough about that for now...
I won't get into too many specific details of the MVP pattern here, since a lot of folks have already covered that quite nicely. Rather I'm going to talk about a refactoring I performed in one of our MVP implementations.
The Problem
Here is a typical example of a presenter that takes some data entered by the user about a person and saves it to the system.
PersonPresenterTests
1 [TestFixture]
2 public class PersonPresenterTests
3 {
4 private Mockery _mockery;
5
6 [SetUp]
7 public void BeforeTest()
8 {
9 _mockery = new Mockery();
10 }
11
12 [TearDown]
13 public void AfterTest()
14 {
15 _mockery.VerifyAllExpectationsHaveBeenMet();
16 }
17
18 [Test]
19 public void ShouldSavePersonDetailsToSystem()
20 {
21 IPersonView mockView = _mockery.NewMock<IPersonView>();
22 IPersonService mockService = _mockery.NewMock<IPersonService>();
23
24 Expect.Once.On(mockView).GetProperty("FirstName").Will(Return.Value("first"));
25 Expect.Once.On(mockView).GetProperty("MiddleName").Will(Return.Value("middle"));
26 Expect.Once.On(mockView).GetProperty("LastName").Will(Return.Value("last"));
27 Expect.Once.On(mockService).Method("SavePerson");
28
29 PersonPresenter presenter = new PersonPresenter(mockView, mockService);
30 presenter.SaveView();
31 }
32
33 [Test]
34 public void ShouldShowErrorMessageWhenExceptionOccursDuringSave()
35 {
36 IPersonView mockView = _mockery.NewMock<IPersonView>();
37 IPersonService mockService = _mockery.NewMock<IPersonService>();
38
39 Expect.Once.On(mockView).GetProperty("FirstName").Will(Return.Value("first"));
40 Expect.Once.On(mockView).GetProperty("MiddleName").Will(Return.Value("middle"));
41 Expect.Once.On(mockView).GetProperty("LastName").Will(Return.Value("last"));
42 Expect.Once.On(mockService).Method("SavePerson").Will(Throw.Exception(new Exception()));
43 Expect.Once.On(mockView).Method("DisplayError");
44
45 PersonPresenter presenter = new PersonPresenter(mockView, mockService);
46 presenter.SaveView();
47 }
48 }
PersonPresenter
1 public class PersonPresenter
2 {
3 private readonly IPersonView _view;
4 private readonly IPersonService _service;
5
6 public PersonPresenter(IPersonView view, IPersonService service)
7 {
8 _view = view;
9 _service = service;
10 }
11
12 public void SaveView()
13 {
14 try
15 {
16 _service.SavePerson(CreatePersonFromView());
17 }
18 catch (Exception e)
19 {
20 _view.DisplayError(string.Format("Error while saving: {0}", e.Message));
21 }
22 }
23
24 private PersonDTO CreatePersonFromView()
25 {
26 return new PersonDTO(_view.FirstName, _view.MiddleName, _view.LastName);
27 }
28 }
IPersonView
1 public interface IPersonView
2 {
3 string FirstName { get; }
4 string MiddleName { get; }
5 string LastName { get; }
6 }
PersonDTO
1 public class PersonDTO
2 {
3 private readonly string _firstName;
4 private readonly string _middleName;
5 private readonly string _lastName;
6
7 public string FirstName
8 {
9 get { return _firstName; }
10 }
11
12 public string MiddleName
13 {
14 get { return _middleName; }
15 }
16
17 public string LastName
18 {
19 get { return _lastName; }
20 }
21
22 public PersonDTO(string firstName, string middleName, string lastName)
23 {
24 _firstName = firstName;
25 _middleName = middleName;
26 _lastName = lastName;
27 }
28 }
IPersonService
1 public interface IPersonService
2 {
3 void SavePerson(PersonDTO person);
4 }
Take notice the private method named CreatePersonFromView in the presenter class which handles creating a new PersonDTO from the properties on the view. So this is fine as long as the number of properties on your view remain small. The overhead of having the expectations in your tests to retrieve the property values from the view isn't that big of a deal when you only have 3 of them.
But let's say perhaps you have a typical data entry view with 15-20 properties on it. That would mean for each test in your presenter test fixture that exercised the SaveView method on the presenter, you'd most likely need to duplicate those property getter expectations in each one (or of course extract them out into a private method in your test fixture). But then the question arises, should the presenter be responsible for mapping the view to a DTO in this case, or should that behavior be delegated to a separate object?
In comes Extract View Mapper. Since Google has no knowledge of it, I'm going to be cocky and say I created this new refactoring...joking of course... :P (Knowing full well that a lot of people have probably already done something very similar...)
In order to decrease the complexity of the presenter tests and the presenter class itself, you can extract out the logic that maps a view to a DTO behind an interface so it can be mocked in your presenter tests. It also simplifies your presenter class since all the mapping logic is delegated to a different object. So enough talk, let's see some code.
(For the sake of brevity, I've left out the some of the skeleton code shown above...)
PersonPresenterTests
1 [Test]
2 public void ShouldSavePersonDetailsToSystem()
3 {
4 IPersonView mockView = _mockery.NewMock<IPersonView>();
5 IPersonService mockService = _mockery.NewMock<IPersonService>();
6 IPersonViewMapper mockViewMapper = _mockery.NewMock<IPersonViewMapper>();
7
8 PersonDTO personDTO = new PersonDTO("first", "middle", "last");
9
10 Expect.Once.On(mockViewMapper).Method("MapToDTO").With(mockView).Will(Return.Value(personDTO));
11 Expect.Once.On(mockService).Method("SavePerson").With(personDTO);
12
13 PersonPresenter presenter = new PersonPresenter(mockView, mockService, mockViewMapper);
14 presenter.SaveView();
15 }
16
17 [Test]
18 public void ShouldShowErrorMessageWhenExceptionOccursDuringSave()
19 {
20 IPersonView mockView = _mockery.NewMock<IPersonView>();
21 IPersonService mockService = _mockery.NewMock<IPersonService>();
22 IPersonViewMapper mockViewMapper = _mockery.NewMock<IPersonViewMapper>();
23
24 PersonDTO personDTO = new PersonDTO("first", "middle", "last");
25
26 Expect.Once.On(mockViewMapper).Method("MapToDTO").With(mockView).Will(Return.Value(personDTO));
27 Expect.Once.On(mockService).Method("SavePerson").Will(Throw.Exception(new Exception()));
28 Expect.Once.On(mockView).Method("DisplayError");
29
30 PersonPresenter presenter = new PersonPresenter(mockView, mockService, mockViewMapper);
31 presenter.SaveView();
32 }
PersonPresenter
1 public class PersonPresenter
2 {
3 private readonly IPersonView _view;
4 private readonly IPersonService _service;
5 private readonly IPersonViewMapper _viewMapper;
6
7 public PersonPresenter(IPersonView view, IPersonService service, IPersonViewMapper viewMapper)
8 {
9 _view = view;
10 _service = service;
11 _viewMapper = viewMapper;
12 }
13
14 public void SaveView()
15 {
16 try
17 {
18 _service.SavePerson(_viewMapper.MapToDTO(_view));
19 }
20 catch (Exception e)
21 {
22 _view.DisplayError(string.Format("Error while saving: {0}", e.Message));
23 }
24 }
25 }
IPersonViewMapper
1 public interface IPersonViewMapper
2 {
3 PersonDTO MapToDTO(IPersonView view);
4 }
So as you can see, our presenter tests and presenter class itself has been simplified to use the new view mapper to handle mapping the values from the view to the DTO. You can also see that the private method CreatePersonFromView is no longer needed in the PersonPresenter class. Anytime I can extract out logic from a private method in a class to another new or existing class, it’s a good thing because it allows it to be directly covered by a unit test instead of indirectly covered through a classes public interface.
Again, this is probably overkill for a simple example such as the one posted above; it was simply used to demonstrate how the refactoring was performed. I’m sure there are different and/or better ways to accomplish this, but this is pretty close to how I performed this refactoring in one of our real applications. The only difference is that in the real application I made a generic IViewMapper<ViewType, DTOType> interface so that the same interfact could be used for all view mappers.
Anyways, that’s all for now. I’d love to hear your feedback on this and how you’ve tackled the problem of test complexity with mock objects using the MVP pattern.