We have created a .Net Core Api project before, this post will discuss how to do the unit testing for the project.
First question, why should we do the unit testing?
Here are some key reasons why unit testing is important and worth investing:
Finds bugs early – Unit tests help catch bugs early in the development cycle, when they are less expensive to fix.
Improves design – Writing unit tests forces you to think about how your code will be used and drive more modular, reusable code.
Facilitates change – Good unit tests allow developers to refactor code quickly and verifiably. Tests help ensure changes don’t break existing functionality.
Reduces risks – Tests provide a safety net to catch edge cases and errors. They build confidence that refactoring and changes won’t introduce hard-to-find bugs.
Documents code – Unit tests document how production code should be used. They are live examples of how to call various methods.
Enables automation – Automated testing catches issues quickly and frees developers from manual testing. Tests can be run on every code change.
Improves collaboration – Unit tests enable multiple developers to collaborate on code using a shared suite of tests to ensure changes don’t cause issues.
Avoids technical debt – Lack of tests creates a technical debt that slows future development. Writing tests avoid this debt.
As I have mentioned before, I like to use VS Code
, so I will also base on VS Code
and xUnit Test
to demonstrate how to do it! 🙂
If you have installed the VS Code
extension vscode-solution-explorer
then you can easy to create a xUnit Test
project from the solution explorer. Just right click the solution and “Add a new project”
then use the xUnit Test Project
template, and input the project name MyDemo.Test
, then will be created a xUnit Test
project
We need to use a mock framework for creating fake objects that simulate the behavior of real objects for testing. So we can install Moq
from Nuget
for our testing project
Before we start, we need to create the mock object for repository
. Because the mock framework needs to inject into the function logic for testing, so we must use the interfaces programming for the project (like a repository pattern, so this is one of the benefits of using a repository pattern :)).
We can create a MockIUserRepository
class to handle user repository testing
//MyDemo.Test/Mocks/MockIUserRepository.cs
using System.Linq.Expressions;
using Moq;
using MyDemo.Core.Data.Entity;
using MyDemo.Core.Repositories;
namespace MyDemo.Test.Mocks
{
public class MockIUserRepository
{
//todo...
}
}
because we use IQueryable
for GetAll
in our repository method
public IQueryable<T> GetAll() => _context.Set<T>().AsNoTracking();
so we also need to return the same object for a mock object, then we create the user data as below
public static IQueryable<User> GetUsers()
{
var users = new List<User>() {
new User() {
Id = 100,
Name = "Unit Tester 1",
Email = "unit.tester1@abc.com",
IsActive = true,
CreatedAt = DateTime.Now.AddDays(-2),
UpdatedAt = DateTime.Now.AddDays(-2)
},
new User() {
Id = 200,
Name = "Unit Tester 2",
Email = "unit.tester2@abc.com",
IsActive = true,
CreatedAt = DateTime.Now.AddDays(-1),
UpdatedAt = DateTime.Now.AddDays(-1)
}
}.AsQueryable();
return users;
}
then we create the mock instance for IUserRepository
var mock = new Mock<IUserRepository>();
setup the mock instance to return a list of users when the GetAll
method is called
//get the fake user data
var users = GetUsers();
//setup the GetAll and return the user data
mock.Setup(u => u.GetAll()).Returns(()=> users);
setup the GetItemWithConditionAsync
and GetWithConditionAsync
methods, because these methods need to pass an expression
parameter for query data, so we also need to use the Expression<Func<User, bool>
for mock the parameter, and this is an async
method, so need to use ReturnsAsync
for return the value
mock.Setup(u => u.GetItemWithConditionAsync(It.IsAny<Expression<Func<User, bool>>>())).
ReturnsAsync((Expression<Func<User, bool>> expr) => users.FirstOrDefault(expr));
mock.Setup(u => u.GetWithConditionAsync(It.IsAny<Expression<Func<User, bool>>>())).
ReturnsAsync((Expression<Func<User, bool>> expr) => users.Where(expr));
the complete codes as below
//MyDemo.Test/Mocks/MockIUserRepository.cs
using System.Linq.Expressions;
using Moq;
using MyDemo.Core.Data.Entity;
using MyDemo.Core.Repositories;
namespace MyDemo.Test.Mocks
{
public class MockIUserRepository
{
public static Mock<IUserRepository> GetMock()
{
var mock = new Mock<IUserRepository>();
var users = GetUsers();
mock.Setup(u => u.GetAll()).Returns(()=> users);
mock.Setup(u => u.GetItemWithConditionAsync(It.IsAny<Expression<Func<User, bool>>>())).
ReturnsAsync((Expression<Func<User, bool>> expr) => users.FirstOrDefault(expr));
mock.Setup(u => u.GetWithConditionAsync(It.IsAny<Expression<Func<User, bool>>>())).
ReturnsAsync((Expression<Func<User, bool>> expr) => users.Where(expr));
return mock;
}
public static IQueryable<User> GetUsers()
{
var users = new List<User>() {
new User() {
Id = 100,
Name = "Unit Tester 1",
Email = "unit.tester1@abc.com",
IsActive = true,
CreatedAt = DateTime.Now.AddDays(-2),
UpdatedAt = DateTime.Now.AddDays(-2)
},
new User() {
Id = 200,
Name = "Unit Tester 2",
Email = "unit.tester2@abc.com",
IsActive = true,
CreatedAt = DateTime.Now.AddDays(-1),
UpdatedAt = DateTime.Now.AddDays(-1)
}
}.AsQueryable();
return users;
}
}
}
Ok, now we can create the first testing case to get all user data. Get the mock instance and call the repository GetAll
method
[Fact]
public void Test1_GetUsers()
{
//get the mock instance
var mockUserRepository = MockIUserRepository.GetMock();
//call the repository to get user data
var users = mockUserRepository.Object.GetAll();
//test the result
//the users should not be null
Assert.NotNull(users);
//the users should be an IQueryable<User> object
Assert.IsAssignableFrom<IQueryable<User>>(users);
//should be only 2 items in the user list (this is the fake data that we created)
Assert.Equal(2, users.Count());
}
VS Code
can be very helpful to add the actions on the testing case method, you can click the Run Test
or Debug Test
to run the test
if everything is ok, you will see the testing result in terminal
This time we want to test the method GetUser
of UserController
. We need to create the user controller instance first, as you know there is a constructor for receiving two parameters in the user controller, that’s means we also need to pass these when new it
public UserController(IUserRepository userRepository, ILogger<User> logger)
{
this._userRepository = userRepository;
this._logger = logger;
}
but how should we pass the repository and logger objects? Ok, we can also mock them:
//get the user repository mock instance
var mockUserRepository = MockIUserRepository.GetMock();
//create the ILogger mock instance
var logger = Mock.Of<ILogger<User>>();
now we can new a user controller instance
var userController = new UserController(mockUserRepository.Object, logger);
call the GetUser
from controller, because the fake user id is 100
and 200
, so we try to pass 100 to get a user
//get the user data by id
var result = userController.GetUser(100);
//convert the result to ObjectResult so that we can check the status codes
var objResult = result.Result.Result as ObjectResult;
//convert the result value to our define's ApiResult object, and we can get the user data
var apiResult = objResult.Value as ApiResult<User>;
//test the result values
Assert.NotNull(result);
Assert.Equal(StatusCodes.Status200OK, objResult.StatusCode);
Assert.IsAssignableFrom<User>(apiResult.Data);
Assert.Equal("Unit Tester 1", apiResult.Data.Name);
the complete codes as below
[Fact]
public void Test2_GetUserById()
{
var mockUserRepository = MockIUserRepository.GetMock();
//mock the logger
var logger = Mock.Of<ILogger<User>>();
var userController = new UserController(mockUserRepository.Object, logger);
var result = userController.GetUser(100);
var objResult = result.Result.Result as ObjectResult;
var apiResult = objResult.Value as ApiResult<User>;
Assert.NotNull(result);
Assert.Equal(StatusCodes.Status200OK, objResult.StatusCode);
Assert.IsAssignableFrom<User>(apiResult.Data);
Assert.Equal("Unit Tester 1", apiResult.Data.Name);
}
You should know the logic and concept, so I just show you the codes below
[Fact]
public void Test3_CreateUser()
{
var mock = MockIUserRepository.GetMock();
//mock the logger
var logger = Mock.Of<ILogger<User>>();
var userController = new UserController(mock.Object, logger);
//call the Post method to create a user
var result = userController.PostUser(new User()
{
Id = 300,
Name = "new user",
Email = "newTester@abc.com",
IsActive = true,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
});
var objResult = result.Result.Result as ObjectResult;
var apiResult = objResult.Value as ApiResult<User>;
Assert.NotNull(result);
Assert.Equal(StatusCodes.Status200OK, objResult.StatusCode);
Assert.Equal(true, apiResult.Success);
Assert.Equal(300, apiResult.Data.Id);
}
The codes as below
[Fact]
public void Test4_UpdateUser()
{
var mock = MockIUserRepository.GetMock();
//mock the logger
var logger = Mock.Of<ILogger<User>>();
var userController = new UserController(mock.Object, logger);
var result = userController.PutUser(new User()
{
Id = 100,
Name = "update user",
Email = "updateTester@abc.com",
IsActive = true,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
});
var objResult = result.Result.Result as ObjectResult;
var apiResult = objResult.Value as ApiResult<User>;
Assert.NotNull(result);
Assert.Equal(StatusCodes.Status200OK, objResult.StatusCode);
Assert.Equal(true, apiResult.Success);
Assert.Equal("update user", apiResult.Data.Name);
}
Because there is no user data returned by the delete function, so we just need to check the success status from the Api
[Fact]
public void Test5_DeleteUserById()
{
var mock = MockIUserRepository.GetMock();
//mock the logger
var logger = Mock.Of<ILogger<User>>();
var userController = new UserController(mock.Object, logger);
var result = userController.DeleteUser(100);
var objResult = result.Result as ObjectResult;
var apiResult = objResult.Value as ApiResult<Object>;
Assert.NotNull(result);
Assert.Equal(StatusCodes.Status200OK, objResult.StatusCode);
//check the success whether is true
Assert.Equal(true, apiResult.Success);
}
In the end, we can run all tests on the class level
and find the result below
We learned how to do the unit test with the xUnit Test
and how to mock the data. There is an important thing you should know if you want to mock data for testing, you must use the interface programming and dependency injection, so if you call the get user method directly from a help method, then you can’t mock it for testing, you can put the method into service and implement from an interface.
The other thing that needs to note is that must use the same parameter type with your method when you setup a mock instance.
Please let me know if you have any ideas for that! 😀
The post How to do Unit Testing for the Core Api Project with Repository first appeared on Coder Blog.