catalogue
2. Why do you need unit testing
3.2 try to avoid direct testing of proprietary methods
3.5 document and method naming specification
4. Introduction to common class libraries
5. Use of Visual Studio in practice
5.1 how to run unit tests in Visual Studio
5.2 how to view unit test coverage in Visual Studio
6. Mock of common scenes in practice
1. Preface
Unit testing has always been a difficult problem of "everyone knows a lot of benefits, but it has not been implemented for various reasons". Each project has its own situation as to whether the unit test should be implemented and the degree of implementation.
This article is "how to write unit tests better", that is, it is more inclined to practice and shares some theories with others.
The unit test framework of the following example is xUnit and the mock library is Moq
2. Why do you need unit testing
There are many advantages. Here are two obvious advantages I personally think
2.1 prevention of regression
Usually, during the development or reconstruction of new functions / modules, the test will regression test the existing functions to verify whether the previously implemented functions can still run as expected.
With unit tests, you can rerun a complete 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 feel so obvious. Writing tests for code will naturally decouple the code and improve the code quality and maintainability in a disguised form.
3. Basic principles and norms
3.1 3A principle
3A are "range, act and assert" respectively, which represent the three stages of a qualified unit test method
-
Prior preparation
-
Actual call of test method
-
Assertion on return value
Readability of a unit test method is one of the most important aspects when writing tests. Separating these operations in the test will clearly highlight the dependencies required to call the code, how to call the code, and what to try to assert
Therefore, when writing unit tests, please use comments to mark the of each stage 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); }
3.2 try to avoid direct testing of proprietary methods
Although private methods can be tested directly through reflection, in most cases, it is not necessary to test private private methods directly, but to verify private private methods by testing public public methods.
It can be said that private methods will never exist in isolation. We should be more concerned about the final result of the public method calling the private method.
3.3 reconstruction principle
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. Whether there is a possibility of decoupling. Selectively reconstruct the original method instead of writing it down
3.4 avoid multiple assertions
If there are multiple assertions in a test method, one or more assertions may fail, resulting in the failure of the whole method. In this way, we can't fundamentally know the reason for the test failure.
So there are generally two solutions
-
Split into multiple test methods
-
Use parametric testing, as shown 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); }
Of course, if you assert an object, you may assert multiple attributes of the object. This is an exception.
3.5 document and method naming specification
File name specification
There are generally two. For example, unit tests for methods under UserController should be uniformly placed in UserControllerTest or UserController_Test next
Unit test method name
The method name of unit test should be readable so that the whole test method can be read without annotation. The format should be similar to the following
<Full name of the method under test>_<Expected results>_<Conditions given> // example [Fact] public void Add_InputNullOrAlphabetic_ThrowsArgumentException() { ... }
4. Introduction to common class libraries
4.1 xUnit/MsTest/NUnit
to write. Net Core's unit test can't be bypassed. One of the three unit test frameworks should be selected
-
MsTest is a test framework officially produced by Microsoft
-
NUnit hasn't been used
-
xUnit is Net Foundation, and it is a unit testing framework used by many warehouses (including runtime) on dotnet github
The development of the three test frameworks has been very poor so far. Many times, the choice only depends on personal preferences.
Personal preference xUnit concise assertion
// xUnit Assert.True() Assert.Equal() // MsTest Assert.IsTrue() Assert.AreEqual()
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
-
https://github.com/moq/moq4
Moq is a very popular simulation library. As long as there is an interface, it can dynamically generate an object. 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 { ... } } }
During unit testing, you can use Moq pair_ userService.GetUser simulated return value
[Fact] public void GetUser_ShouldReturnNotFound_WhenCannotFoundUser() { // arrange // Create a mock object for IUserService var mockUserService = new Mock<IUserService>(); // moq is used to mock the GetUs method of IUserService: null is returned 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()); }
4.3 AutoFixture
Official warehouse
-
https://github.com/AutoFixture/AutoFixture
AutoFixture is a pseudo data filling library, which aims to minimize the range stage in 3A, 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
Create strongly typed fake data directly in the following way
[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); }
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); }
5. Use of Visual Studio in practice
Visual Studio provides complete unit test support, including running to write. Debug unit tests. And check the unit test coverage.
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, which is not available in community version.
How to view coverage
-
In the test window, right-click the corresponding test group
-
Click "analyze code coverage" below
6. Mock of common scenes in practice
main
6.1 DbSet
In the process of using EF Core, how to mock DbSet is an unavoidable barrier.
Method 1
Refer to the answers in the following links for self encapsulation
https://stackoverflow.com/questions/31349351/how-to-add-an-item-to-a-mock-dbset-using-moq
Method 2 (recommended)
Use ready-made libraries (also encapsulated based on the above method)
Warehouse address:
-
https://github.com/romantitov/MockQueryable
Usage example
// 1. Create a simulated list < T > during test var users = new List<UserEntity>() { new UserEntity{LastName = "ExistLastName", DateOfBirth = DateTime.Parse("01/20/2012")}, ... }; // 2. Convert to dbset < userentity > var mockUsers = users.AsQueryable().BuildMock(); // 3. Users attribute in DbContext assigned to mock var mockDbContext = new Mock<DbContext>(); mockDbContext .Setup(it => it.Users) .Return(mockUsers);
6.2 HttpClient
Scenes using restease / refine
If a third-party library such as RestEase or refine is used, the definition of the specific interface is essentially an interface, so you can directly use moq to mock the method.
This method is recommended.
IHttpClientFactory
If yes Net Core's own IHttpClientFactory to request external interfaces, you can refer to the following methods to mock the IHttpClientFactory
https://www.thecodebuzz.com/unit-test-mock-httpclientfactory-moq-net-core/
6.3 ILogger
Since the LogError and other methods of ILogger belong to extension methods, there is no need for special method level mock.
A help class is encapsulated for some normal use scenarios. The following help 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); } }
usage method
[Fact] 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()); }
7. Expansion
7.1 TDD introduction
TDD is the English abbreviation of test driven development. It generally designs various scenarios of unit test in advance, then writes real business code, and weaves a safety net to strangle bugs in the cradle.
This development mode takes testing first, has high requirements for the development team, and there may be many practical difficulties in landing. Please refer to the following for details
https://www.guru99.com/test-driven-development.html
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