designing technology that makes business sense

.Net MVC2 – Unit Testing with Moq

Posted on August 18, 2010 13:46 by wilk

We continue our series of experiments using the infamous Nerd Dinner application as our reference implementation.  Last time we added Dependency Injection using StructureMap.  This time we will look at unit testing with Moq.  Like the dependency injection last week, the unit testing strategies used on Nerd Dinner work fine for an application the size of Nerd Dinner.  However, my goal in this series of experiments is to try to understand how to apply various alternative technologies and how these technologies impact larger real-world applications. The Nerd Dinner application is relatively small and it already has about 100 unit tests.  What happens when there are 500 or a 1000 units tests?  I realize that like lines of code, the number of units tests by itself is meaningless.  However, assuming you continue to follow your methodology, as an application gets larger and more complicated, both the number of lines of code and the number of unit tests go up.

Moq

Moq is a mocking framework used in unit testing.  Several mocking frameworks are available, and a interesting comparisons are available here and here.  If you are familiar with a mocking frameworks, you should consider using what you know.  I am going to discuss the implications of using mocks versus fakes in MVC applications, but I am not going to cover anything that is not supported by most leading mock frameworks.  Let’s look at a test from the DinnerControllerTest.

[TestMethod]
public void DetailsAction_Should_Return_View_For_Dinner() {
  // Arrange
  var controller = CreateDinnersController();
  // Act
  var result = controller.Details(1);
  // Assert
  Assert.IsInstanceOfType(result, typeof(ViewResult));
}

First, in the Arrange section, the test requests a controller instance from a central CreateDinnersController().  I am torn on this practice. Yes, this supports a DRY principle.  Changes to the controller portion of the test can be made in one place.  On the flip side, in unit tests, I am more concerned with unit of work, keeping the test simple, and only setting up what needs to be set-up for the particular test in question.  With a centralize factory method for test, the method tends to get complicated by trying to support all the conditions of all the tests.  In the case above, these complications are moved to fakes, but the complications exist just the same.  While the complications can generally be handled by the mocking framework, these conditions are programmed by humans working under deadlines.  One of the fundamental principles of JIT manufacturing and quality control is to make it easier to do things right than to do them wrong.  On the other side of this double-edged sword, if you put your controller instantiation in each test, and you add a new dependency to a controller, you now have many places to change your test code – maybe.

Let’s look at the CreateDinnersController() method.

DinnersController CreateDinnersController() {
  var testData = FakeDinnerData.CreateTestDinners();
  var repository = new FakeDinnerRepository(testData);
  return new DinnersController(repository);
}

What should a controller be?

In many tutorials of the MVC framework, the model contains the data, the view presents data, and the controller contains the business logic to prepare the data for the view.  When reading this discussion on fakes vs. mocks, keep in mind this is not my view of MVC.

I sometimes have a flexible morality when it comes to code, but I have some strong beliefs on MVC.  I believe in thin Controllers; they are traffic cops, if you play soccer, they are the center midfielder responsible for distributing the ball to the appropriate players.  I also believe in view models that exactly match the view. If I am to use fakes, I will more likely use them on models and view models to assure the delivery of well formed objects.  However, on controller actions, I want to know the controller is behaving as expected.

This method uses a static method FakeDinnerData.CreateTestDinners() to generate test data. It then creates a FakeDinnerRepository to inject into the DinnersController.  Fakes are not mocks, and Martin Fowler and his team provide a concise explanation of the difference. In over simplified terms, Fakes are used to test state, mocks are used to test behavior. 

In NerdDinner, the FakeDinnerRepository is simple, however, fakes can become more complicated.  To pass a test, the developer now not only needs to worry that the controller is functioning properly but also that the fake is delivering correct and well formed results to the controller.  When testing an action of the controller, I want to know the controller is calling a repository with correct parameters and the correct number of times – that it behaves as expected.  I don’t care if my repository (or fake repository) are behaving properly; I have separate unit test for that.

Here is where I will start with Moq.  Despite my concerns with the centralized CreateDinnersController method, I will keep it.  I will also keep the FakeDinnerData class.  Test data is not needed in our first test, but it will be needed later.  I don’t want to create test data code any more than I need to.  I will replace the FakeDinnerRepository with a mock repository.  The CreateDinnersController method now becomes:

DinnersController CreateDinnersController(string userName = null) {
  var testData = FakeDinnerData.CreateTestDinners();
  _mockDinner = new Mock<Dinner>();
  _mockDinnerRepo = new Mock<IDinnerRepository>();
  _mockDinnerRepo.Setup(dr => dr.GetDinner(It.IsInRange(1, 1, Range.Inclusive)))
                 .Returns(_mockDinner.Object);
  return new DinnersController(_mockDinnerRepo.Object);
}

Is this more lines of code? Depends how much code was in the FakeDinnerRepository. In this method, I am creating a mock Dinner and a mock IDinnerRepository.  I then describe the behavior I want this repository to have – if the repository’s GetDinner method is called with a parameter in the range of 1 to 1 (inclusive), then return the mock dinner as a Dinner object.  _mockDinner is a mock object, but _mockDinner.Object returns a Dinner object.  Our new controller is then injected with our mock IDinnerRepository.  Our first test, shown above, remains the same.  However, we can now add an additional test to verify that the controller behaved as expected.  Specifically, did the controller call dinnerRepository.GetDinner with an integer parameter, and did it call .GetDinner once and only once.  We add the following Assert to our test:

_mockDinnerRepo.Verify(x => x.GetDinner(It.IsAny<int>()),Times.Exactly(1));

The next test “DetailsAction_Should_Return_NotFoundView_For_BogusDinner” sends a bad dinnerID to the Details action of the Dinner controller.  The controller should still behave properly as above, but our mock will not.  The Controller expects the repository to return null when GetDinner is given a bad dinnerID, but we only told our mock how to behave when the dinner ID is 1.  Let’s tell the mock a little more about is behavior.  In the CreateDinnersController, we add the following setup statement:

_mockDinnerRepo.Setup(dr => dr.GetDinner(It.IsInRange(2, 1000, Range.Inclusive)))
               .Returns<Dinner>(null);

Now when .GetDinner is called with any integer value between 2 and 100 (inclusive), null will be returned.  And like the first test, we can add our Verify to confirm the controller faithfully called .GetDinner once and only once.  Now for the third test - don’t worry, I am not going through all 96 unit tests one at a time. Just where we are adding something new.  This test checks the controller’s Edit method, but the controller comes from a CreateDinnersControllerAs(string) method.  The Edit action requires the user identity from the HttpContext, and then it calls the CreateDinnerController method to create the controller as before.  Extending the centralized controller creation in this way is one way to mitigate my concerns of overly complicated centralized methods.

[TestMethod]
public void EditAction_Should_Return_View_For_ValidDinner() {
  // Arrange
  var controller = CreateDinnersControllerAs("SomeUser");
  // Act
  var result = controller.Edit(1) as ViewResult;
  // Assert
  Assert.IsInstanceOfType(result.ViewData.Model, typeof(Dinner));
}

The NerdDinner application already uses Moq, and it uses it in the CreateDinnerControllerAs method to create the ControllerContext.  We will leave this unchanged, but our mock dinner now needs to know how to behave when the .IsHostedBy method is called with a user’s name.  We need to get the user’s name to where we are setting up the mock’s behavior, but I don’t want to have to pass in a user’s name into the CreateDinnerController method on all of these test where I don’t need it.  To solve this, I am going to use a new feature of C# 4.0, the optional parameter.  I have to admit, this is one feature I like from VB.  Yes, I said it, I used to program in VB, but that’s nothing.  I used to program in COBOL, Pascal, Assembler and APL.  Anyone out there ever program matrix math in APL – fugly!  Our CreateDinnerController becomes…

DinnersController CreateDinnersController(string userName = null) {
  var testData = FakeDinnerData.CreateTestDinners();
  _mockDinner = new Mock<Dinner>();
  _mockDinner.Setup(d => d.IsHostedBy("SomeUser")).Returns(true);
  _mockDinnerRepo = new Mock<IDinnerRepository>();
  _mockDinnerRepo.Setup(dr => dr.GetDinner(It.IsInRange(1, 1, Range.Inclusive))).Returns(_mockDinner.Object);
  _mockDinnerRepo.Setup(dr => dr.GetDinner(It.IsInRange(2, 1000, Range.Inclusive))).Returns<Dinner>(null);
  return new DinnersController(_mockDinnerRepo.Object);
}


Two changes.  First, the userName was added as an optional input parameter to our controller creator.  Second, our mock dinner is told how to behave when it’s .IsHostedBy method is called and is passed the string “SomeUser”.  We can also add a second Verify to our test to confirm that the IsHostedBy method is called once and only once.  The fourth test is almost the same, but we add a “bad user” behavior to our mock dinner with the following line:

_mockDinner.Setup(d => d.IsHostedBy("SomeOtherUser"))
           .Returns(false);

The next few tests offer some variation to the above, but really offer nothing new relating to mocking until we get to “IndexAction_Should_Return_View”.  The index action returns a list of dinners.  Remember FakeDinnerData.CreateTestDinners() in our creator that we kept but were not using, well now we want it.  The CreateTestDinners returns a List<Dinner> of test dinner objects, and this list is assigned to our testDinners variable.  To support the Index action tests, we add the following line to our mock IDinnerRepository set-up:

_mockDinnerRepo.Setup(dr => dr.FindUpcomingDinners())
               .Returns(testDinners.AsQueryable());

Let’s look at what we’ve done.  We eliminated the FakeDinnerRepository, we’ve added several behaviors to our CreateDinnerController method, and we added some additional asserts (Verify) to our tests to confirm Controller behavior.  We are creating a list of test dinners on every test (in CreateDinnerController) even if we don’t use them, and we kept the existing method that sets up the controller context when needed.  What happens if you extrapolate these test strategies out to a medium or large application? 

What happens with a controller with 200 tests and 4 injected repositories?  The CreateDinnerController method becomes more complex.  This complexity can be managed, but we are making it easier to make a mistake.  Also, we may very likely be creating a large amount of test data and not always using it.  We can mitigate the complexity by moving the controller creation into each individual test so we are only creating the test data and behaviors needed for each individual test.  However, this solution has its own set of problems.  Let’s say a 5th repository needs to be injected into the controller, or the behavior of a repository method changes, we would now have many places to change code.  In my case, I think I’m smart, and I know I’m lazy, so I almost always go with the centralized solution.  However, I’m torn.  There has to be a better way.  Next up, AutoMocking.


Comments

August 23. 2010 19:06

hello im from germany and my english isnt that great, but i was able to understand every  word of your post. Im reading english websites to make my english better and im very euphoric to finally find a blog, that writes clear and structured english that i can translate. Greetz from Germany!

Computer Lautsprecher United States

August 24. 2010 17:48

Totally agree. Came across this great site whilst trying to find a decent one to comment on. Well done!

Mark Jones United States

August 24. 2010 22:28

Hi! Can I use pics from your blog for my degree work?

After effects United States

August 24. 2010 23:58

Thx for taking the time out to write this article, I look forward to many more.

Jackson Pusch United States

August 26. 2010 00:12

We have to admit that here the content deservesa lot of congrats!

mobbing United States

August 30. 2010 01:19

Simply want to say your article is as tonishing. The clarity in your post is simply impressive and i can take for granted you are an expert on this field. Well with your permission allow me to grab your rss feed to keep up to date with forthcoming post. Thanks a million and please keep up the a uthentic work.

Jessa United States

September 1. 2010 01:28

Substantially, the article is in reality the sweetest on this laudable topic. I concur with your conclusions and will eagerly look forward to your next updates. Saying thanks will not just be adequate, for the extraordinary clarity in your writing. I will right away grab your rss feed to stay privy of any updates. Good work and much success in your business enterprize!

Jaquenetta United States

September 2. 2010 11:37

I bookmarked this blog a while ago because of the great content and I am never being dissapointed. Keep up the good work.

bölgesel zay?flama United States

September 2. 2010 14:03

I bookmarked this blog a while ago because of the good content and I am never being dissapointed. Keep up the good work.

bölgesel zayöflama United States

September 2. 2010 17:48

try to change your template, there are some great ones...

gift ideas United States

September 2. 2010 19:57

Coolblog but I had some problems watching it in Internet Explorer. Got any clue why?

bölgesel zayiflama United States

September 3. 2010 14:54

any plan to update this blog?

ETF newsletter United States

September 3. 2010 16:08

try to change your template, there are some great ones...

gift idea United States

September 4. 2010 00:48

Well, the post is in reality the freshest on this worthy topic. I fit in with your conclusions and will thirstily look forward to your approaching updates. Just saying thanks will not just be adequate, for the phenomenal clarity in your writing. I will right away grab your rss feed to stay informed of any updates. De lightful work and much success in your business efforts!

Jenna United States

September 4. 2010 01:15

do you have a xml feed for your blog?

dating tips United States

September 4. 2010 07:30

Not a bad post, did it take you a large number of your time to consider it?

theft insurance United States

September 4. 2010 07:48

nice blog, try posting more!

ETF newsletter United States

September 4. 2010 11:43

nice blog, try posting more!

ETF newsletter United States

September 4. 2010 21:43

I differ with most people here; I started reading this blog post I couldn't stop until I was done, although it wasn't just what I had been trying to find, was indeed a fantastic read though. I will instantly grab your feed to maintain informed of future updates.

Free PSN Codes United States

September 5. 2010 03:24

how old is this post?

dating tips United States

September 5. 2010 07:04

do you have a different blog where you post more often?

ETF United States

September 6. 2010 16:38

For most individuals, making ends meet can be very difficult. It is very disheartening that most individuals are living from check to check.

payday loan news Serbia and Montenegro (Former)

September 6. 2010 19:39

This is my favorite posts from you, I've read nearly all of your games threads and find them thrilling do you love arcade games as much as me? You should do a thread on one of those next time

Jason Smith United States

September 7. 2010 14:33

nice blog but it deserve a better theme...

stock newsletters United States

September 8. 2010 13:20

how old is this post?

stock newsletter United States

September 8. 2010 18:33

Super post, great tips

car insurance for young driver United States

September 8. 2010 23:06

I found so many interesting stuff in your blog especially its discussion. From the tons of comments on your articles, I guess I am not the only one having all the enjoyment here! Keep up the good work.

Free Microsoft Points United States

September 9. 2010 02:19

Hi! I found your blog on AOL.It's really comprehensive  and it helped me a lot.

Continue the good work!

wow leveling United States

September 9. 2010 18:17

This is great post. Thanks

wireless pc gamepad United States

September 9. 2010 20:28

Well it seems like this article is quite involved, congratulations to the owner! I try to read as much as possible online when I have the time. Unfortunatley it is becomming commonplace for individuals to pass all the night on the net instead of actually experiencing life, what a shame. All the same, continue with the awesome articles in any event...leastwise the masses can have select literature to fertilize thier croaking brains. Perhaps its better off, the general people are not very intelligent at any rate.

free business cards United States

September 10. 2010 20:23

I've been looking around for information on dating and came across your blog, thanks for the info I'll be bookmarking you.

Sexy Dating United States

September 11. 2010 11:21

Definitely concur with exactly what you stated. Your explanation was certainly the least complicated to comprehend.  I usually get irritated any time folks comment on issues that these people obviously don't know about. You managed to hit the nail on the head and spelled out the whole thing with out problem. Maybe, people can take a sign. Will likely be back to get more. Many thanks

Todd Walker United States

September 13. 2010 02:05

There are so many blogs around relating to dating, it's always great to find some fresh content

Dating Site United States

September 13. 2010 05:38

hey, nice blog…really like it and added to bookmarks. keep up with good work.

Sean Moriera United States

September 16. 2010 20:37

Amazing !   Great resource.  Thanks.

Tony Salvati United States

September 17. 2010 04:32

I really appreciate your blog. Excellent job!

healthcare administrative United States

September 17. 2010 05:53

Well it seems like this website is quite involved, hooray to the author! I attempt to read as much as possible online when I have enough time. Unfortunatley it is becomming common for souls to expend all day online instead of actually experiencing life, what a shame. Anyhow, continue with the great articles anyhow...at any rate the flocks can have quality content to fertilize thier perishing brains. Maybe its better off, the general people arn't very intelligent in any case.

free pens United States

Comments are closed