How should one unit test a .NET MVC controller?

73,328

Solution 1

A controller unit test should test the code algorithms in your action methods, not in your data layer. This is one reason to mock those data services. The controller expects to receive certain values from repositories / services / etc, and to act differently when it receives different information from them.

You write unit tests to assert the controller behaves in very specific ways in very specific scenarios / circumstances. Your data layer is one piece of the app that provides those circumstances to the controller / action methods. Asserting that a service method was called by the controller is valuable because you can be certain that the controller gets the information from another place.

Checking the type of the viewmodel returned is valuable because, if the wrong type of viewmodel is returned, MVC will throw a runtime exception. You can prevent this from happening in production by running a unit test. If the test fails, then the view may throw an exception in production.

Unit tests can be valuable because they make refactoring much easier. You can change the implementation, and assert that the behavior is still the same by making sure all of the unit tests pass.

Answer to comment #1

If changing the implementation of a method-under-test calls for the change / removal of a lower-layer mocked method, then the unit test must also change. However, this shouldn't happen as often as you may think.

The typical red-green-refactor workflow calls for writing your unit tests before writing the methods they test. (This means for a brief amount of time, your test code won't compile, and is why many young / inexperienced developers have difficulty adopting red green refactor.)

If you write your unit tests first, you will come to a point where you know the controller needs to get information from a lower layer. How can you be certain it tries to get that information? By mocking out the lower layer method that provides the information, and asserting that the lower-layer method is invoked by the controller.

I may have misspoke when I used the term "changing implementation." When a controller's action method & corresponding unit test must be altered to change or remove a mocked method, you are really changing the behavior of the controller. Refactoring, by definition, means changing the implementation without altering the overall behavior and expected results.

Red-green-refactor is a Quality Assurance approach that helps prevent bugs & defects in code before they ever appear. Typically developers change implementation to remove bugs after they appear. So to reiterate, the cases you are worried about should not happen as often as you think.

Solution 2

You should first put your controllers on a diet. Then you can have fun unit testing them. If they are fat and you have stuffed all your business logic inside them, I agree that you will be passing your life mocking stuff around in your unit tests and complaining that this is a waste of time.

When you talk about complex logic, this doesn't necessarily mean that this logic cannot be separated in different layers and each method be unit tested in isolation.

Solution 3

Yes, you should test all the way to the DB. The time you put into mocking is less and the value you get from mocking is very less too(80% of likely errors in your system cannot be picked by mocking).

And this is a great article discussing the benefits of integration testing over unit testing because "unit testing kills!" (it says)

When you test all the way from a controller to DB or web service then it is not called unit testing but integration testing. I personally believe in integration testing as opposed to unit testing, even though they both serve different purposes.. even though we need both unit testing and integration testing. 'Time constraints' are real, so writing both will not be practical hence just stick to Integration Tests alone. And I am able to do test-driven development successfully with integration tests(scenario testing).

Here is how it works for our team. Every test class in the beginning regenerates DB and populates/seeds the tables with minimum set of data(eg: user roles). Based on a controllers need we populate DB and verify if the controller does it's task. This is designed in such a way that DB corrupt data left by other methods will never fail a test. Except time take to run, pretty much all qualities of unit test(even though it is a theory) are gettable. Time taken to sequentially run can be reduced with containers. Also with containers, we don't need to recreate DB as every test gets its own fresh DB in a container(which will be removed after the test).

There were only 2% situations(or very rarely) in my career when I was forced to use mocks/stubs as it was not possible to create a more realistic data source. But in all other situations integration tests was a possibility.

It took us time to reach a matured level with this approach. we have a nice framework which deals with test data population and retrieval(first class citizens). And it pays off big time! First step is to say goodbye to mocks and unit tests. If mocks do not make sense then they are not for you! Integration test gives you good sleep.

===================================

Edited after a comment below: Demo

Integration test or functional test has to deal with DB/source directly. No mocks. So these are the steps. You want to test getEmployee( emp_id). all these 5 steps below are done in a single test method.

  1. Drop DB

  2. Create DB and populate roles and other infra data

  3. Create an employee record with ID

  4. Use this ID and call getEmployee(emp_id)// this could an api-url call (that way db connection string need not be maintained in a test project, and we could test almost all environment by simply changing domain names)

  5. Now Assert()/ Verify if the returned data is correct

    This proves that getEmployee() works . Steps until 3 requires you to have code used only by test project. Step 4 calls the application code. What I meant is creating an employee (step 2) should be done by test project code not application code. If there is an application code to create employee (eg: CreateEmployee()) then this should not be used. Same way, when we test CreateEmployee() then GetEmployee() application code should not be used. We should have a test project code for fetching data from a table.

This way there are no mocks! The reason to drop and create DB is to prevent DB from having corrupt data. With our approach, the test will pass no matter how many times we run it.

Special Tip: In step 5 getEmployee() returns an employee object. If later a developer removes or changes a field name the test breaks. What if a developer adds a new field later? And he/she forgets to add a test for it (assert)? Test would not pick it up. The solution is to add a field count check. eg: Employee object has 4 fields (First Name, Last Name, Designation, Sex). So Assert number of fields of employee object is 4. So when new field is added our test will fail because of the count and reminds the developer to add an assert field for the newly added field.

Solution 4

The point of a unit test is to test the behaviour of a method in isolation, based on a set of conditions. You set the conditions of the test using mocks, and assert the method's behaviour by checking how it interacts with other code around it -- by checking which external methods it tries to call, but particularly by checking the value it returns given the conditions.

So in the case of Controller methods, which return ActionResults, it is very useful to inspect the value of the returned ActionResult.

Have a look at the section 'Creating Unit Tests for Controllers' here for some very clear examples using Moq.

Here is a nice sample from that page which tests that an appropriate view is returned when the Controller attempts to create a contact record and it fails.

[TestMethod]
public void CreateInvalidContact()
{
    // Arrange
    var contact = new Contact();
    _service.Expect(s => s.CreateContact(contact)).Returns(false);
    var controller = new ContactController(_service.Object);

    // Act
    var result = (ViewResult)controller.Create(contact);

    // Assert
    Assert.AreEqual("Create", result.ViewName);
}

Solution 5

I don't see much point in unit testing the controller, since it is usually just a piece of code that connects other pieces. Unit testing it typically includes lots of mocking and just verifies that the other services are connected correctly. The test itself is a reflection of the implementing code.

I prefer integration tests -- I start not with a concrete controller, but with an Url, and verify that the returned Model has the correct values. With the help of Ivonna, the test might look like:

var response = new TestSession().Get("/Users/List");
Assert.IsInstanceOf<UserListModel>(response.Model);

var model = (UserListModel) response.Model;
Assert.AreEqual(1, model.Users.Count);

I can mock the database access, but I prefer a different approach: setup an in-memory instance of SQLite, and recreate it with each new test, together with the required data. It makes my tests fast enough, but instead of complicated mocking, I make them clear, e.g. just create and save a User instance, rather than mock the UserService (which might be an implementation detail).

Share:
73,328

Related videos on Youtube

ChaseMedallion
Author by

ChaseMedallion

Updated on July 05, 2022

Comments

  • ChaseMedallion
    ChaseMedallion almost 2 years

    I'm looking for advice regarding effective unit testing of .NET mvc controllers.

    Where I work, many such tests use moq to mock the data layer and to assert that certain data-layer methods are called. This doesn't seem useful to me, since it essentially verifies that the implementation has not changed rather than testing the API.

    I've also read articles recommending things like checking that the type of view model returned is correct. I can see that providing some value, but alone it doesn't seem to merit the effort of writing many lines of mocking code (our application's data model is very large and complex).

    Can anyone suggest some better approaches to controller unit testing or explain why the above approaches are valid/useful?

    Thanks!

  • ChaseMedallion
    ChaseMedallion over 12 years
    I agree with much of what you say, but I don't understand how the unit test can help when changing the implementation (your last point) if it requires the implementation to call certain lower-level methods (your second point).
  • ChaseMedallion
    ChaseMedallion over 11 years
    I still don't totally agree with the idea of testing that a function calls another function, but I understand where you're coming from when you think about it from a "tests first" perspective. Also, you're probably right that in many cases a change in service calls does indicate a change in controller behavior. Thanks for the very thoughtful, descriptive, and well-reasoned response!
  • Piotr Lewandowski
    Piotr Lewandowski over 10 years
    I think you missunderstand the idea behind Unit testing. Unit test are supposed to test a piece of code (unit) in separation from other layers/dependencies etc. In your case you should write separate tests fot MVC routes, controller action and service. This what you described here are end-to-end tests, and a good tools for that kind of tests is e.g. SpecFlow and Watin.
  • ulu
    ulu over 10 years
    Thanks, I know that. Note that I wrote "I prefer integration tests". WatiN won't let me investigate my model, for example.
  • Erik Philips
    Erik Philips about 9 years
    The link put your controllers on a diet is missing :(
  • ryanulit
    ryanulit about 9 years
    I think this is the link Darin was pointing to: youtube.com/watch?v=8TYJjRxXnXs
  • Mariusz
    Mariusz about 8 years
    I like your approach. I prefer integration testing also as it can show me if the system works. Unit test seems to deal with logical part only which is fine but doesn't seem enough for me.
  • Bart
    Bart about 8 years
    I thought I'd edit and changed the link to yours, great work @ryanulit !
  • Muflix
    Muflix about 7 years
    It is better to Unit test a database and then have simple integration test in mvc test project, or do you have one integration test in the mvc test project which also test the returned data ?
  • Blue Clouds
    Blue Clouds about 7 years
    yes. Integration test or functinal test has to deal with DB directly. No mocks. I have edited the answer with a demo steps.
  • CSR
    CSR about 6 years
    Like the integration tests approach and i am also thinking in similar way. The primary goal is, to make sure that, our app wont broken, when changes are made, but not to write unit tests with mocks and others.
  • rsenna
    rsenna almost 6 years
    "since it is usually just a piece of code that connects other pieces" I guess you could say that about any piece of code at all.
  • Kyle Vassella
    Kyle Vassella over 5 years
    One of the most concrete, and only overviews of how to integration test a controller on the entire interwebs, believe it or not. Thank you @BlueClouds ... thank you. Answers the question I wrote here: stackoverflow.com/questions/53014850/…
  • xr280xr
    xr280xr almost 4 years
    It shouldn't be a binary choice between unit testing or integration testing. Both are important. Integration tests the interaction between all of your various "units" and is definitely important, and usually feels easier to test. But without unit testing, it can be nearly impossible to test thoroughly because of all of possible permutations of variable states. In other words, you can't truly verify the parts all work correctly together before verifying the parts work as expected individually and that's where unit testing comes in.
  • Blue Clouds
    Blue Clouds over 2 years
    If we can we should write and maintain both. Choice arises when we realize we don't have the time to do both.
  • Blue Clouds
    Blue Clouds over 2 years
    Exactly! Time constraint prevents us from writing and maintaining both. This is a great article discussing the benefits of integration testing over unit testing because "unit testing kills!" (it says) linkedin.com/pulse/…