In the previous article, we learned how to write Unit Tests by using the xUnit and the different attributes that xUnit provides for us. We’ve also seen how to test validation rules inside a single validation class.
But what about controllers and all the actions inside? Can we write tests for them too?
In this article, we are going to talk more about testing controllers in ASP.NET Core applications by using Moq and unit tests.
For the complete navigation of this series, you can visit ASP.NET Core Testing.
So, let’s get going.
Unit Testing Controllers Using Moq Library
Before we start, let’s take a look at the EmployeesController’s constructor code:
As you can see, we are using Dependency Injection to inject the interface into our controller. So basically, our controller has a dependency on the repository logic through that injected interface.
And there is no problem with that approach at all, it is even recommended. But when we write tests for our controller or any other class in a project, we should isolate those dependencies.
There are several advantages to isolating dependencies in a test code:
- We don’t have to initialize all dependencies to return correct values, thus making our test code much simplified
- If our test fails and we didn’t isolate dependency, we can’t be sure whether it fails due to some error in a controller or in that dependency
- When dependent code communicates with a real database, as our repository does, the test code could take more time to execute. This can happen due to connection issues or simply due to the time needed to fetch the data from the database.
Of course, there are additional reasons to isolate dependencies in test code, but you get the point.
That said, let’s install the Moq library in the EmployeesApp.Tests
project:
Install-Package Moq
After the installation completes, we are going to create a new Controller
folder in the same project and add EmployeesControllerTests
class.
Creating a Mock Object
Let’s modify the EmployeesControllerTests
class:
public class EmployeesControllerTests { private readonly Mock<IEmployeeRepository> _mockRepo; private readonly EmployeesController _controller; public EmployeesControllerTests() { _mockRepo = new Mock<IEmployeeRepository>(); _controller = new EmployeesController(_mockRepo.Object); } }
We create a mock object of type IEmployeeRepository
inside the constructor, and since we want to test the controller logic, we create an instance of that controller with the mocked object as a required parameter.
And there you go. Everything is prepared, and a dependency is mocked, so all we have to do is to write some tests.
Unit Testing the Index Action
If we take a look at the Index action in the EmployeesController
class, we can see that we fetch all employees from the database and return a view with those employees:
public IActionResult Index() { var employees = _repo.GetAll(); return View(employees); }
As a result, we can write a couple of tests to verify that this action is doing exactly what it is supposed to do. Also, you will see that testing controllers with unit tests is not that hard at all.
In the first test, we are going to verify that the Index
action returns a result of type ViewResult
:
[Fact] public void Index_ActionExecutes_ReturnsViewForIndex() { var result = _controller.Index(); Assert.IsType<ViewResult>(result); }
So, we are executing the Index
action from our controller and accepting the result inside the result
variable. After that, we check the type of the returned result with the IsType
method. If the result is of the ViewResult
type the test will pass, otherwise, it will fail.
In this test method, we didn’t use a mocked object because we didn’t use any of our repository methods. We have just checked the type of our result.
Let’s run the Test Explorer window and find out the result:
We can see that our test passes and that the result is of ViewResult
type.
Now, let’s continue by writing another test method to verify that our Index
action returns an exact number of employees:
[Fact] public void Index_ActionExecutes_ReturnsExactNumberOfEmployees() { _mockRepo.Setup(repo => repo.GetAll()) .Returns(new List<Employee>() { new Employee(), new Employee() }); var result = _controller.Index(); var viewResult = Assert.IsType<ViewResult>(result); var employees = Assert.IsType<List<Employee>>(viewResult.Model); Assert.Equal(2, employees.Count); }
In this test method, we are fetching the data from the database by using the GetAll
repository method. Of course, we don’t want to use the concrete repository but the mocked one and therefore we use the Setup
method to specify a setup for the GetAll
method. Additionally, we have to use the Returns
method to specify the value to return from the mocked GetAll
method.
After we store the result of the Index action, we check the type of that result, the type of the model object inside that result, and finally the number of employees by using the Equal
method. All three verifications have to pass in order for the test to pass, otherwise, the test will fail.
Once we run the test, we can see it passes and we verify that the Index action returns exactly two employees.
Testing Create Actions
We have two Create
actions in our EmployeesController
class, the GET, and the POST action. The first action just loads the Create View and that is something we have to test.
So, let’s do it:
[Fact] public void Create_ActionExecutes_ReturnsViewForCreate() { var result = _controller.Create(); Assert.IsType<ViewResult>(result); }
We already had a test like this, just with the Index
action, so, there is nothing new about it. If we run Test Explorer we can verify that the test passes.
Let’s move on to the second Create action, the POST one. In that action, we have a model validation and if it is invalid we return a view with the employee object.
So let’s test that:
[Fact] public void Create_InvalidModelState_ReturnsView() { _controller.ModelState.AddModelError("Name", "Name is required"); var employee = new Employee { Age = 25, AccountNumber = "255-8547963214-41" }; var result = _controller.Create(employee); var viewResult = Assert.IsType<ViewResult>(result); var testEmployee = Assert.IsType<Employee>(viewResult.Model); Assert.Equal(employee.AccountNumber, testEmployee.AccountNumber); Assert.Equal(employee.Age, testEmployee.Age); }
We have to add a model error to the ModelState
property in order to test an invalid model state.
After that, we create a new employee without the Name
property, which makes it invalid as well.
Finally, we call the create action and execute a couple of assertions.
With assert statements, we verify that the result is of type ViewResult
and that the model is of the Employee
type. Additionally, we are making sure that we get the same employee back by comparing property values from the testEmployee
and the employee objects:
Additional Invalid Model Test
Let’s write one additional test to verify that the CreateEmployee
method, from our repository, never executes if the model state is invalid:
[Fact] public void Create_InvalidModelState_CreateEmployeeNeverExecutes() { _controller.ModelState.AddModelError("Name", "Name is required"); var employee = new Employee { Age = 34 }; _controller.Create(employee); _mockRepo.Verify(x => x.CreateEmployee(It.IsAny<Employee>()), Times.Never); }
The first three lines of code are the same as in the previous test method, we add a model error, create an invalid employee object, and call the Create
action from our controller.
Of course, in that action, we have the CreateEmployee
method which shouldn’t be executed if the model is invalid. That is exactly what we verify with the Verify
method from the mocked object. By using the It.IsAny<Employee>
expression, we state that it doesn’t matter which employee is passed as a parameter to the CreateEmployee
method. The only important thing is that the parameter is of the Employee
type.
The last parameter of the Verify
method is the number of times our method executes. We use Times.Never
because we don’t want to execute the CreateEmployee
method at all if the model state is invalid.
You can run the test to see that it passes.
If we modify the test code by placing the Times.Once
instead of Times.Never
the test will fail for sure. The message that you get explains pretty well what is the problem and why the test fails.
Testing if Model is Valid in the Create Action
If the Model State is valid the CreateEmployee
method should be executed just once:
[Fact] public void Create_ModelStateValid_CreateEmployeeCalledOnce() { Employee? emp = null; _mockRepo.Setup(r => r.CreateEmployee(It.IsAny<Employee>())) .Callback<Employee>(x => emp = x); var employee = new Employee { Name = "Test Employee", Age = 32, AccountNumber = "123-5435789603-21" }; _controller.Create(employee); _mockRepo.Verify(x => x.CreateEmployee(It.IsAny<Employee>()), Times.Once); Assert.Equal(emp.Name, employee.Name); Assert.Equal(emp.Age, employee.Age); Assert.Equal(emp.AccountNumber, employee.AccountNumber); }
So, we set up the CreateEmployee
method with any employee and use the Callback
method to populate the emp
object with the values from the employee parameter. After that, we create a local employee
object, execute the Create
action with that employee
and Verify
if the CreateEmployee
method has been executed just once.
Additionally, we verify that the emp
object is the same as the employee
object provided to the Create
action.
Finally, let’s run that in the Test Explorer:
One more test to go:
[Fact] public void Create_ActionExecuted_RedirectsToIndexAction() { var employee = new Employee { Name = "Test Employee", Age = 45, AccountNumber = "123-4356874310-43" }; var result = _controller.Create(employee); var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result); Assert.Equal("Index", redirectToActionResult.ActionName); }
If we take a look at the Create
action in our controller, we are going to see that after creating a new employee, we redirect a user to the Index
action.
Well, that is exactly what we’ve successfully tested in our test method.
Conclusion
That is it. As you can see, testing controllers can be quite an easy job when you have the right tool to do that. We have used the Index and Create actions to show you how to test our controller and actions inside it, but all the rules can be applied to other types of Actions (PUT, DELETE…).
So, to sum up, we have learned:
- How to use the Moq library to isolate dependency in the test code
- To write different tests for our actions inside a controller
In the next part, we are going to talk about integration tests and how to create an in-memory database.