On Net Core backend unit test

Posted by abolmeer on Sat, 01 Jan 2022 19:41:26 +0100

catalogue

1. Preface

2. Why do you need unit testing

2.1 prevention of regression

2.2 reduce code coupling

3. Basic principles and norms

3.1 3A principle

3.2 try to avoid direct testing of proprietary methods

3.3 reconstruction principle

3.4 avoid multiple assertions

3.5 document and method naming specification

4. Introduction to common class libraries

4.1 xUnit/MsTest/NUnit

4.2 Moq

4.3 AutoFixture

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

6.1 DbSet

6.2 HttpClient

6.3 ILogger

7. Expansion

7.1 TDD introduction

 

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

Topics: ASP.NET .NET unit testing