Comparing expectations with Constraints using Rhino Mocks
I've been more work with Rhino lately (and really digging it) and wanted to pass on a tidbit that might be useful to some of you.
Here's the setup. You're implementing the MVP pattern and your view will contain a list of items (probably displayed in a grid, listbox, etc.). You start with a test where your presenter will initialize the view with a list of items from a backend system. You want your test to verify that the view gets updated (via the presenter) with the correct list.
Let's setup our test using mocks. We'll mock out the view since the implementation isn't important to us right now. We only want to ensure that when the presenter is initialized (via a called to OnViewReady) it will
- Call a backend system to get a list of items
- Set the view with those list of items
- We can ensure the view contains those items.
Here's the test for that with excessive comments so it should be self explanatory:
24 [Test]
25 public void OnViewReady_ViewShouldContainItemsFromRepository()
26 {
27 // Create the mock objects
28 MockRepository mocks = new MockRepository();
29 IProjectListView listView = mocks.CreateMock<IProjectListView>();
30
31 // Creat a real presenter and assign the mock view to it
32 ProjectListViewPresenter listViewPresenter = new ProjectListViewPresenter();
33 listViewPresenter.View = listView;
34
35 // Setup our expectations for the view by getting
36 // a list of projects from a repository (created via a factory)
37 RepositoryFactory factory = new RepositoryFactory();
38 ProjectRepository repository = (ProjectRepository)factory.GetRepository<Project>();
39 List<Project> fakeProjectList;
40 fakeProjectList = repository.GetAll();
41 listView.Projects = fakeProjectList;
42 LastCall.IgnoreArguments();
43 Expect.Call(listView.Projects).Return(fakeProjectList);
44
45 // Invoke the mock engine
46 mocks.ReplayAll();
47
48 // Call the real presenter that we want to test
49 // This will call the repository to retrieve a list of
50 // projects and update the view to include them
51 listViewPresenter.OnViewReady();
52
53 // Check the view to ensure it's been updated
54 // with the list of projects from the repository
55 Assert.AreEqual(3, listView.Projects.Count);
56
57 // Teardown the mocks
58 mocks.VerifyAll();
59 }
Other than the setup code (creating the mocks, presenter, etc.) which could all be done once in a Setup method there's a few problems with checking the list of projects from the view. While we did mock out the view, we're checking the count only. If we want to ensure that the list contains the projects we expect we might have to iterate through the list and compare the Project objects that are returned and that's a lot of work.
There's an easier way to check lists with Rhino which measures up to the same amount of code but is much more effective at testing the contents of the list. We can use constraints on the expected call. Constraints is a list of constraint objects attached to your mock that tell you there are expectations that should match up to the constraint you define.
Instead of assigning our fake project list to our view then setting the expectation via an Expect.Call method, we'll add a constraint to the mock. We don't need to assign the listView.Projects value so we can set it to null and the next line, LastCall.Constraints(...) will be our contraints we want to attach to that mock item:
30 listView.Projects = null;
31 LastCall.Constraints(new AbstractConstraint[] { List.Equal(fakeProjectList) });
We still have to create the expectation, but now we use List.Equal(...) and pass in the fakeProjectList. This means when the mock runs it will ensure that the listView.Projects property not only contains the same number of items (like our original test) but the list is actually of the same type and is the same list (I think Rhino internally uses the Equals method on each item to compare them). So now our test looks like this:
14 [Test]
15 public void OnViewReadyM_ViewShouldContainItemsFromRepository()
16 {
17 // Create the mock objects
18 MockRepository mocks = new MockRepository();
19 IProjectListView listView = mocks.CreateMock<IProjectListView>();
20
21 // Creat a real presenter and assign the mock view to it
22 ProjectListViewPresenter listViewPresenter = new ProjectListViewPresenter();
23 listViewPresenter.View = listView;
24
25 // Set the expectations of the mock via a constraint
26 RepositoryFactory factory = new RepositoryFactory();
27 ProjectRepository repository = (ProjectRepository)factory.GetRepository<Project>();
28 List<Project> fakeProjectList;
29 fakeProjectList = repository.GetAll();
30 listView.Projects = null;
31 LastCall.Constraints(new AbstractConstraint[] { List.Equal(fakeProjectList) });
32
33 // Invoke the mock engine
34 mocks.ReplayAll();
35
36 // Call the real presenter that we want to test
37 // This will call the repository to retrieve a list of
38 // projects and update the view to include them
39 listViewPresenter.OnViewReady();
40
41 // Teardown the mocks
42 mocks.VerifyAll();
43 }
Note that we also don't have to do the Assert.AreEqual check at the end. Our call to VerifyAll will handle that when the mock is torn down.
Like I said, the amount of code isn't that much different, you're just using a different call on setting up the mock. However you can add as many constraints as you like. For example you can ensure the list is of the right type, it contains that many items, and you can even tell it the first item has to have some pre-determined value. Constraints are stacked up and evaluated when you tear the mock down so make sure you put in the VerifyAll call.
This is just an example of changing the way you might check a list of items using constraints but you can use constraints for any type. For example if you had a single property on a mocked object you can add a constraint that it was greater than or equal to a certain value, or that it met a certain type if you were say testing a sub-class. Check out the "Is" class to do things like Is.GreaterThan, Is.LessThanOrEqual, Is.Type, etc.
Lots of possibilities here so dig in and play with constraints on your mocks. You might find something interesting and learn something new along the way!