Talking about the .Net Core unit test. Unit testing has always been an old and difficult problem that “everyone knows a lot of benefits, but has not been implemented for various reasons”.
1 .Net Core unit test
Unit testing has always been an old and difficult problem that “everyone knows a lot of benefits, but has not been implemented for various reasons”. Whether unit testing should be implemented, and the degree of implementation, each project has its own situation.
This article is my personal opinion “how to better write unit tests”, that is, it is more biased towards practice and mixed with some theoretical sharing.
The unit test framework of the following example is xUnit
, the Mock library isMoq
2. Why do we need unit testing
There are many advantages, here are two points that I personally think are obvious advantages
2.1 Prevent regression
Usually when developing new functions/modules or refactoring, the test will perform regression testing of the original existing functions to verify whether the previously implemented functions can still operate as expected.
Using unit testing, you can re-run the entire set of tests after each generation or even after changing a line of code, which can greatly reduce regression defects.
2.2 Reduce code coupling
When the code is tightly coupled or a method is too long, it becomes difficult to write unit tests. When you don’t do unit testing, the coupling of code may not be so obvious. Writing tests for the code will naturally decouple the code and improve code quality and maintainability in disguise.
3. Basic principles and norms
3.1 3A Principle
3A is “arrange, act, assert”, which respectively represent the three stages of a qualified unit test method
- Preliminary preparation
- The actual call of the test method
- Assertions on the return value
The readability of a unit test method is one of the most important aspects when writing tests. Separating these operations in the test clearly highlights the dependencies needed to call the code, how the code is called, and what it is trying to assert.
So when writing a unit test, please use comments to mark the stages of 3A, as shown in the following example
[Fact] public async Task VisitDataCompressExport_ShouldReturnEmptyResult_WhenFileTokenDoesNotExist() { // arrange var mockFiletokenStore = new Mock<IFileTokenStore>(); mockFiletokenStore .Setup(it => it.Get(It.IsAny<string>())) .Returns(string.Empty); var controller = new StatController( mockFiletokenStore.Object, null); // act var actual = await controller.VisitDataCompressExport("faketoken"); // assert Assert.IsType<EmptyResult>(actual); }
.Net Core unit test
3.2 Try to avoid testing private methods directly
Although private methods can be tested directly through reflection, in most cases, there is no need to directly test private private methods, but to verify private private methods by testing public public methods.
Think of it this way: private methods will never exist in isolation. What should be more concerned is the final result of the public method that calls the private method.
3.3 Principles of Refactoring
If a class/method has many external dependencies, it is difficult to write unit tests. Then you should consider whether the current design and dependencies are reasonable. Is there a possibility of decoupling in some parts? Selectively reconstruct the original method, rather than bite the bullet and write it down.
3.4 Avoid multiple assertions
If there are multiple assertions for a test method, one or several assertions may fail, causing the entire method to fail. This inability to fundamentally know the reason for the test failure.
So there are generally two solutions
- Split into multiple test methods
- Use parameterized testing, as in the following example
[Theory] [InlineData(null)] [InlineData("a")] public void Add_InputNullOrAlphabetic_ThrowsArgumentException(string input) { // arrange var stringCalculator = new StringCalculator(); // act Action actual = () => stringCalculator.Add(input); // assert Assert.Throws<ArgumentException>(actual); }
.Net Core unit test
Of course, if you make an assertion on an object, you may have assertions on multiple properties of the object. This is an exception.
3.5 File and method naming convention
File name specification
There are generally two types. For example UserController
, the unit tests for the following methods should be unified UserControllerTest
or UserController_Test
under
Unit test method name
The method name of the unit test should be readable, so that the entire test method can be read without the need for comments. The format should be similar to the following
<Full name of the tested method>_<Expected result>_<Conditions given> // example [Fact] public void Add_InputNullOrAlphabetic_ThrowsArgumentException() { ... }
.Net Core unit test
4. Introduction to Common Class Libraries
4.1 xUnit/MsTest/NUnit
Writing a unit test for .Net Core can’t get around the choice of a unit test framework, among the three major unit test frameworks
- MsTest is a testing framework officially produced by Microsoft
- Never used NUnit
- xUnit is an open source project under .Net Foundation and a unit testing framework used by many repositories (including runtime) on dotnet github
The development of the three major test frameworks is not bad so far. In many cases, the choice is only based on personal preference.
xUnit
Concise assertion of personal preference
// xUnit Assert.True() Assert.Equal() // MsTest Assert.IsTrue() Assert.AreEqual()
.Net Core unit test
To objectively and functionally analyze the differences between the three frameworks, you can refer to the following
https://anarsolutions.com/automated-unit-testing-tools-comparison
4.2 Moq
Official warehouse
Moq is a very popular simulation library, as long as it has an interface, it can dynamically generate an object, and the bottom layer uses Castle’s dynamic proxy function.
Basic usage
In actual use, there may be the following scenarios
public class UserController { private readonly IUserService _userService; public UserController(IUserService userService) { _userService = userService; } [HttpGet("{id}")] public IActionResult GetUser(int id) { var user = _userService.GetUser(id); if (user == null) { return NotFound(); } else { ... } } }
.Net Core unit test
When performing unit testing, you can use the Moq
pair _userService.GetUser
to simulate the return value
[Fact] public void GetUser_ShouldReturnNotFound_WhenCannotFoundUser() { // arrange // Create a mock object of IUserService var mockUserService = new Mock<IUserService>(); // Use moq to mock the GetUs method of IUserService: return null when the input parameter is 233 mockUserService .Setup(it => it.GetUser(233)) .Return((User)null); var controller = new UserController(mockUserService.Object); // act var actual = controller.GetUser(233) as NotFoundResult; // assert // Verify that the GetUser method of userService has been called once, and the input parameter is 233 mockUserService.Verify(it => it.GetUser(233), Times.AtMostOnce()); }
.Net Core unit test
4.3 AutoFixture
Official warehouse
AutoFixture is a fake data filling library designed to minimize the 3A arrange
stages, making it easier for developers to create objects containing test data, so that they can focus more on the design of test cases.
Basic usage
Directly use the following method to create strongly typed fake data
[Fact] public void IntroductoryTest() { // arrange Fixture fixture = new Fixture(); int expectedNumber = fixture.Create<int>(); MyClass sut = fixture.Create<MyClass>(); // act int result = sut.Echo(expectedNumber); // assert Assert.Equal(expectedNumber, result); }
.Net Core unit test
The above example can also be combined with the test framework itself, such as xUnit
[Theory, AutoData] public void IntroductoryTest( int expectedNumber, MyClass sut) { // act int result = sut.Echo(expectedNumber); // assert Assert.Equal(expectedNumber, result); }
.Net Core unit test
5. Combined with the use of Visual Studio in practice
Visual Studio provides complete unit test support, including running, writing, and debugging unit tests. And check the unit test coverage and so on.
5.1 How to run unit tests in Visual Studio
5.2 How to view unit test coverage in Visual Studio
The following functions require Visual Studio 2019 Enterprise version, the community version does not have this function.
How to check coverage
- In the test window, right-click the corresponding test group
- Click “Analyze Code Coverage” below
6. Mock of common scenarios in practice
main
6.1 DbSet
In the process of using EF Core, how to mock DbSet is a barrier that cannot be bypassed.
method one
Refer to the answer at the link below to encapsulate it yourself
https://stackoverflow.com/questions/31349351/how-to-add-an-item-to-a-mock-dbset-using-moq
Method two (recommended)
Use ready-made libraries (also packaged based on the above method)
Warehouse Address:
Usage example
// Create a simulated List during testing var users = new List<UserEntity>() { new UserEntity{LastName = "ExistLastName", DateOfBirth = DateTime.Parse("01/20/2012")}, ... }; // 2. Converted to DbSet by extension method var mockUsers = users.AsQueryable().BuildMock(); // 3. Assigned to the Users property in the DbContext of the mock var mockDbContext = new Mock<DbContext>(); mockDbContext .Setup(it => it.Users) .Return(mockUsers);
.Net Core unit test
6.2 HttpClient
Scenarios using RestEase/Refit
If you are using RestEase
or Refit
waiting for a third-party library, the definition of a specific interface is essentially an interface, so you can directly use moq for method mocking.
And it is recommended to use this method.
IHttpClientFactory
If you are using .Net Core’s own IHttpClientFactory
method to request an external interface, you can refer to the following method to IHttpClientFactory
mock
https://www.thecodebuzz.com/unit-test-mock-httpclientfactory-moq-net-core/
6.3 ILogger
Since ILogger’s LogError and other methods are extension methods, there is no need for special method-level mocks.
A helper class is encapsulated for some usual usage scenarios, and the following helper classes can be used for Mock and Verify
public static class LoggerHelper { public static Mock<ILogger<T>> LoggerMock<T>() where T : class { return new Mock<ILogger<T>>(); } public static void VerifyLog<T>(this Mock<ILogger<T>> loggerMock, LogLevel level, string containMessage, Times times) { loggerMock.Verify( x => x.Log( level, It.IsAny<EventId>(), It.Is<It.IsAnyType>((o, t) => o.ToString().Contains(containMessage)), It.IsAny<Exception>(), (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()), times); } public static void VerifyLog<T>(this Mock<ILogger<T>> loggerMock, LogLevel level, Times times) { loggerMock.Verify( x => x.Log( level, It.IsAny<EventId>(), It.IsAny<It.IsAnyType>(), It.IsAny<Exception>(), (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()), times); } }
.Net Core unit test
Instructions
public void Echo_ShouldLogInformation() { // arrange var mockLogger = LoggerHelpe.LoggerMock<UserController>(); var controller = new UserController(mockLogger.Object); // act controller.Echo(); // assert mockLogger.VerifyLog(LogLevel.Information, "hello", Times.Once()); }
.Net Core unit test
7. .Net Core unit test Expansion
7.1 Introduction to TDD
TDD is the English abbreviation of Test-Driven Development. It is generally designed to design various scenarios for unit testing in advance and then write real business code, weaving a safety net to kill bugs in the cradle.
This kind of development mode takes testing first, and has high requirements on the development team, and there may be many practical difficulties in landing. The detailed description can refer to the following
.Net Core unit test Reference link
- https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices
- https://www.kiltandcode.com/2019/06/16/best-practices-for-writing-unit-tests-in-csharp-for-bulletproof-code/
- https://github.com/AutoFixture/AutoFixture