Talking about the .Net Core unit test

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”. 

.Net Core unit test
.Net Core unit test

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 UserControllerTestor UserController_Testunder

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.

xUnitConcise 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 Moqpair _userService.GetUserto 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 arrangestages, 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
.Net Core unit test
.Net Core unit test

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

.Net Core unit test
.Net Core unit test

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 RestEaseor Refitwaiting 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 IHttpClientFactorymethod to request an external interface, you can refer to the following method to IHttpClientFactorymock

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.

unittest
.Net Core unit test

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

https://www.guru99.com/test-driven-development.html

.Net Core unit test Reference link

Leave a Comment