浅谈.Net Core后端单元测试

1. 前言

单元测试一直都是"好处你们都知道不少,可是由于种种缘由没有实施起来"的一个老大难问题。具体是否应该落地单元测试,以及落地的程度, 每一个项目都有本身的状况。html

本篇为我的认为"如何更好地写单元测试", 即更加偏向实践向中夹杂一些理论的分享。git

下列示例的单元测试框架为xUnit, Mock库为Moqgithub

2. 为何须要单元测试

优势有不少, 这里提两点我我的认为的很明显的好处安全

2.1 防止回归

一般在进行新功能/模块的开发或者是重构的时候,测试会进行回归测试原有的已存在的功能,以验证之前实现的功能是否仍能按预期运行。
使用单元测试,可在每次生成后,甚至在更改一行代码后从新运行整套测试, 从而能够很大程度减小回归缺陷。框架

2.2 减小代码耦合

当代码紧密耦合或者一个方法过长的时候,编写单元测试会变得很困难。当不去作单元测试的时候,可能代码的耦合不会给人感受那么明显。为代码编写测试会天然地解耦代码,变相提升代码质量和可维护性。async

3. 基本原则和规范

3.1 3A原则

3A分别是"arrange、act、assert", 分别表明一个合格的单元测试方法的三个阶段visual-studio

  • 事先的准备
  • 测试方法的实际调用
  • 针对返回值的断言

一个单元测试方法可读性是编写测试时最重要的方面之一。 在测试中分离这些操做会明确地突出显示调用代码所需的依赖项、调用代码的方式以及尝试断言的内容.单元测试

因此在进行单元测试的编写的时候, 请使用注释标记出3A的各个阶段的, 以下示例测试

[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 尽可能避免直接测试私有方法

尽管私有方法能够经过反射进行直接测试,可是在大多数状况下,不须要直接测试私有的private方法, 而是经过测试公共public方法来验证私有的private方法。ui

能够这样认为:private方法永远不会孤立存在。更应该关心的是调用private方法的public方法的最终结果。

3.3 重构原则

若是一个类/方法,有不少的外部依赖,形成单元测试的编写困难。那么应该考虑当前的设计和依赖项是否合理。是否有部分能够存在解耦的可能性。选择性重构原有的方法,而不是硬着头皮写下去.

3.4 避免多个断言

若是一个测试方法存在多个断言,可能会出现某一个或几个断言失败致使整个方法失败。这样不能从根本上知道是了解测试失败的缘由。

因此通常有两种解决方案

  • 拆分红多个测试方法
  • 使用参数化测试, 以下示例
[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);
}

固然若是是对对象进行断言, 可能会对对象的多个属性都有断言。此为例外。

3.5 文件和方法命名规范

文件名规范

通常有两种。好比针对UserController下方法的单元测试应该统一放在UserControllerTest或者UserController_Test

单元测试方法名

单元测试的方法名应该具备可读性,让整个测试方法在不须要注释说明的状况下能够被读懂。格式应该相似遵照以下

<被测试方法全名>_<指望的结果>_<给予的条件>

// 例子
[Fact]
public void Add_InputNullOrAlphabetic_ThrowsArgumentException()
{
  ...
}

4. 经常使用类库介绍

4.1 xUnit/MsTest/NUnit

编写.Net Core的单元测试绕不过要选择一个单元测试的框架, 三大单元测试框架中

  • MsTest是微软官方出品的一个测试框架
  • NUnit没用过
  • xUnit是.Net Foundation下的一个开源项目,而且被dotnet github上不少仓库(包括runtime)使用的单元测试框架

三大测试框架发展至今已经是大差不差, 不少时候选择只是靠我的的喜爱。

我的偏好xUnit简洁的断言

// xUnit
Assert.True()
Assert.Equal()

// MsTest
Assert.IsTrue()
Assert.AreEqual()

客观地功能性地分析三大框架地差别能够参考以下

https://anarsolutions.com/automated-unit-testing-tools-comparison

4.2 Moq

官方仓库

Moq是一个很是流行的模拟库, 只要有一个接口它就能够动态生成一个对象, 底层使用的是Castle的动态代理功能.

基本用法

在实际使用中可能会有以下场景

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
        {
            ...
        }
    }
}

在进行单元测试的时候, 可使用Moq_userService.GetUser进行模拟返回值

[Fact]
public void GetUser_ShouldReturnNotFound_WhenCannotFoundUser()
{
    // arrange
    // 新建一个IUserService的mock对象
    var mockUserService = new Mock<IUserService>();
    // 使用moq对IUserService的GetUs方法进行mock: 当入参为233时返回null
    mockUserService
      .Setup(it => it.GetUser(233))
      .Return((User)null);
    var controller = new UserController(mockUserService.Object);
    
    // act
    var actual = controller.GetUser(233) as NotFoundResult;
    
    // assert
    // 验证调用过userService的GetUser方法一次,且入参为233
    mockUserService.Verify(it => it.GetUser(233), Times.AtMostOnce());
}

4.3 AutoFixture

官方仓库

AutoFixture是一个假数据填充库,旨在最小化3A中的arrange阶段,使开发人员更容易建立包含测试数据的对象,从而能够更专一与测试用例的设计自己。

基本用法

直接使用以下的方式建立强类型的假数据

[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);
}

上述示例也能够和测试框架自己结合,好比xUnit

[Theory, AutoData]
public void IntroductoryTest(
    int expectedNumber, MyClass sut)
{
    // act
    int result = sut.Echo(expectedNumber);
    
    // assert
    Assert.Equal(expectedNumber, result);
}

5. 实践中结合Visual Studio的使用

Visual Studio提供了完备的单元测试的支持,包括运行. 编写. 调试单元测试。以及查看单元测试覆盖率等。

5.1 如何在Visual Studio中运行单元测试

5.2 如何在Visual Studio中查看单元测试覆盖率

以下功能须要Visual Studio 2019 Enterprise版本,社区版不带这个功能。

如何查看覆盖率

  • 在测试窗口下,右键相应的测试组
  • 点击以下的"分析代码覆盖率"

6. 实践中常见场景的Mock

主要

6.1 DbSet

使用EF Core过程当中,如何mock DbSet是一个绕不过的坎。

方法一

参考以下连接的回答进行自行封装

https://stackoverflow.com/questions/31349351/how-to-add-an-item-to-a-mock-dbset-using-moq

方法二(推荐)

使用现成的库(也是基于上面的方式封装好的)

仓库地址:

使用范例

// 1. 测试时建立一个模拟的List<T>
var users = new List<UserEntity>()
{
  new UserEntity{LastName = "ExistLastName", DateOfBirth = DateTime.Parse("01/20/2012")},
  ...
};

// 2. 经过扩展方法转换成DbSet<UserEntity>
var mockUsers = users.AsQueryable().BuildMock();

// 3. 赋值给给mock的DbContext中的Users属性
var mockDbContext = new Mock<DbContext>();
mockDbContext
  .Setup(it => it.Users)
  .Return(mockUsers);

6.2 HttpClient

使用RestEase/Refit的场景

若是使用的是RestEase或者Refit等第三方库,具体接口的定义本质上就是一个interface,因此直接使用moq进行方法mock便可。

而且建议使用这种方式。

IHttpClientFactory

若是使用的是.Net Core自带的IHttpClientFactory方式来请求外部接口的话,能够参考以下的方式对IHttpClientFactory进行mock

https://www.thecodebuzz.com/unit-test-mock-httpclientfactory-moq-net-core/

6.3 ILogger

因为ILogger的LogError等方法都是属于扩展方法,因此不须要特别的进行方法级别的mock。
针对平时的一些使用场景封装了一个帮助类, 可使用以下的帮助类进行Mock和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);
    }
}

使用方法

[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. 拓展

7.1 TDD介绍

TDD是测试驱动开发(Test-Driven Development)的英文简称. 通常是先提早设计好单元测试的各类场景再进行真实业务代码的编写,编织安全网以便将Bug扼杀在在摇篮状态。

此种开发模式以测试先行,对开发团队的要求较高, 落地可能会存在不少实际困难。详细说明能够参考以下

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

参考连接

相关文章
相关标签/搜索