How should the unit test the .NET MVC controller be?

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

In cases where I work, many of these tests use moq to make fun of the data layer and claim that certain data-level methods are being called. This does not seem useful to me, since it essentially verifies that the implementation has not changed, and has not tested the API.

I also read articles recommending things like checking the return type of a view model. I see that this brings some value, but apparently it does not deserve the effort of writing many lines of mocking code (our application data model is very large and complex).

Can someone suggest some better approaches for testing controller modules or explain why the above approaches are valid / useful?

Thank!

+53
c # unit-testing asp.net-mvc-3 controller
Jan 11 '12 at 11:08
source share
6 answers

The unit test controller should check the code algorithms in your action methods, and not at your data level. This is one reason to mock these data services. The controller expects to receive certain values ​​from the repositories / services / etc and act differently when it receives various information from them.

You write unit tests to claim that the controller behaves in a very specific way in very specific scenarios / circumstances. The data layer is one part of the application that exposes these circumstances to management / action methods. The assertion that the service method is called by the controller is valuable because you can be sure that the controller receives information from another place.

Checking the type of the returned view model is valuable because if the wrong viewmodel type is returned, MVC will throw an exception at runtime. This can be prevented by running unit test. If the test fails, then the view may cause an exception in production.

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

Reply to comment # 1

If a change in the implementation of the-under-test method causes a change / deletion of the bullying method at the lower level, then the unit test should also change. However, this should not happen as often as you think.

A typical red-green-refactor workflow requires writing your unit tests before writing the methods that they test. (This means that for a short period of time, your test code will not compile, and so many young / inexperienced developers have difficulty accepting a red green refactor.)

If you write your unit tests first, you will reach the point where you know that the controller needs to receive information from a lower level. How can you be sure that he is trying to get this information? Get rid of the lower-level method, which provides information, and claiming that the lower-level method is called by the controller.

Perhaps I was mistaken when I used the term "implementation change". When you need to change the controller action method and the corresponding unit test in order to change or delete the mocked method, you really change the behavior of the controller. Refactoring, by definition, means changing the implementation without changing the overall behavior and expected results.

Red-green-refactor is a quality assurance approach that helps prevent code errors and errors before they appear. Typically, developers change the implementation to remove errors after they appear. Therefore, to repeat, the cases you worry about should not happen as often as you think.

+42
Jan 11 '12 at 11:18
source share

You must first put your controllers on a diet. Then you can have fun testing them. If they are fat and you have filled all your business logic inside yourself, I agree that you will go through your daily life in your unit tests and complain that this is a waste of time.

When you talk about complex logic, this does not necessarily mean that this logic cannot be separated in different layers, and each method must be isolated separately.

+20
Jan 11 '12 at 12:50
source share

The unit test point is to test the behavior of a method individually based on a set of conditions. You set the conditions of the test using mocks and validate the behavior of the method by checking how it interacts with other code around it - by checking which external methods it tries to call, but, in particular, by checking the value that it returns taking into account the conditions.

So, in the case of Controller methods returning ActionResults, it is very useful to check the value of the returned ActionResult.

Take a look at the “Creating Device Tests for Controllers” section here for some very clear examples using Moq.

Here is a good sample from this page that checks that the corresponding view is returned when the controller tries 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); } 
+9
Jan 11 '12 at 11:52
source share

I don’t see much point in unit testing the controller, since it is usually part of the code that connects the other parts. Unit testing usually involves a lot of ridicule and just checks that other services are connected correctly. The test itself is a reflection of the embed code.

I prefer integration tests - I start not with a specific controller, but with Url and check that the returned model has the correct values. With Ivonna, a test might look like this:

 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 make fun of database access, but I prefer a different approach: configure the SQLite instance in memory and recreate it with each new test along with the required data. This makes my tests fast enough, but instead of complicated ridicule, I make them understandable, for example. just create and save the user instance, and don't scoff at the UserService (which may be an implementation detail).

+7
Jan 11 2018-12-12T00:
source share

Yes, you must fully test the database. The time you taunt is less, and the value you get from ridicule is much less (80% of the likely errors in your system cannot be discarded due to ridicule).

When you check all the way from the controller to the database or web service, this is not called unit testing, but integration testing. I personally believe in integration testing, not unit testing. And I can successfully complete the test development.

Here's how it works for our team. Each test class at the beginning restores the database and fills / sorts tables with a minimal set of data (for example, user roles). Based on the needs of the controllers, we fill out the database and check whether the controller performs its task. This is designed in such a way that damaged DB data left by other methods never ends. With the exception of the time that needs to be done, almost all the qualities of a unit test (although this is theory) are gettable.

In my career, there were only 2% of situations (or very rarely) when I was forced to use mocks / stubs, since it was impossible to create a more realistic data source. But in all other situations, integration tests were possible.

It took us a while to reach a mature level with this approach. we have a good structure that concerns the aggregate and search for test data (first-class citizens). And it pays off a lot of time :). The first step is to say goodbye to bullying and unit tests. If ridicule does not make sense, then they are not for you! Integration test gives you a good sleep

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

Edited after the comment below: Demo

An integration test or a functional test must deal with the database directly. No ridicule. So these are the steps. You want to test getEmployee () . All these 5 steps below are performed by one test method.

  • Drop db
  • Creating a database and filling in roles and other information data
  • Create employee record with ID
  • Use this identifier and call getEmployee ()
  • Now Assert () / Check the returned data

    This proves that getEmployee () works. Steps to 3 require that the code be used only by the test project. Step 4 calls up the application code. What I had in mind is the creation of an employee (step 2), should be performed by the code of the project code, and not by the application code. If application code exists to create the employee (for example: CreateEmployee () ), this should not be used. Similarly, when we test CreateEmployee () , then GetEmployee () should not use the application code. We need to have a test project code to retrieve data from the table.

So no bullying! The reason for the failure and creation of the database is the prevention of erroneous database data. With our approach, the test will pass no matter how many times we run it.

Special tip: in step 5, if getEmployee () returns an employee object. If the developer later deletes or changes the field name, the test is interrupted because the fields are checked. What if the developer adds a new field later? And he / she forgets to add a test for him (to approve)? The solution is to always add a check on the number of fields. for example: The Employee object has 4 fields (First Name, Last Name, Designation, Gender). Thus, the number of fields of the employee’s object is 4. And our test will fail due to the calculation and will remind the developer to add a confirmation field for the newly added field. And also our test code will add this new field to the database and receive it and verify it.

And this is a great article that discusses the benefits of unit testing integration testing.

+6
Aug 04 '15 at 9:21
source share

Usually, when you talk about unit tests, you are testing one separate procedure or method, not the whole system, trying to eliminate all external dependencies.

In other words, when testing a controller, you write a testing method by method, and you don’t even need to load a view or model, these are the parts that you need to “mock”. You can then modify the mocks to return values ​​or errors that are difficult to reproduce in other tests.

+1
Jan 11 '12 at 11:18
source share



All Articles