天天看點

淺談.Net Core後端單元測試1. 前言2. 為什麼需要單元測試3. 基本原則和規範4. 常用類庫介紹5. 實踐中結合Visual Studio的使用6. 實踐中常見場景的Mock7. 拓展

目錄

1. 前言

2. 為什麼需要單元測試

2.1 防止回歸

2.2 減少代碼耦合

3. 基本原則和規範

3.1 3A原則

3.2 盡量避免直接測試私有方法

3.3 重構原則

3.4 避免多個斷言

3.5 檔案和方法命名規範

4. 常用類庫介紹

4.1 xUnit/MsTest/NUnit

4.2 Moq

4.3 AutoFixture

5. 實踐中結合Visual Studio的使用

5.1 如何在Visual Studio中運作單元測試

5.2 如何在Visual Studio中檢視單元測試覆寫率

6. 實踐中常見場景的Mock

6.1 DbSet

6.2 HttpClient

6.3 ILogger

7. 拓展

7.1 TDD介紹

1. 前言

單元測試一直都是"好處大家都知道很多,但是因為種種原因沒有實施起來"的一個老大難問題。具體是否應該落地單元測試,以及落地的程度, 每個項目都有自己的情況。

本篇為個人認為"如何更好地寫單元測試", 即更加偏向實踐向中夾雜一些理論的分享。

下列示例的單元測試架構為

xUnit

, Mock庫為

Moq

2. 為什麼需要單元測試

優點有很多, 這裡提兩點我個人認為的很明顯的好處

2.1 防止回歸

通常在進行新功能/子產品的開發或者是重構的時候,測試會進行回歸測試原有的已存在的功能,以驗證以前實作的功能是否仍能按預期運作。

使用單元測試,可在每次生成後,甚至在更改一行代碼後重新運作整套測試, 進而可以很大程度減少回歸缺陷。

2.2 減少代碼耦合

當代碼緊密耦合或者一個方法過長的時候,編寫單元測試會變得很困難。當不去做單元測試的時候,可能代碼的耦合不會給人感覺那麼明顯。為代碼編寫測試會自然地解耦代碼,變相提高代碼品質和可維護性。

3. 基本原則和規範

3.1 3A原則

3A分别是"arrange、act、assert", 分别代表一個合格的單元測試方法的三個階段

  • 事先的準備
  • 測試方法的實際調用
  • 針對傳回值的斷言

一個單元測試方法可讀性是編寫測試時最重要的方面之一。在測試中分離這些操作會明确地突出顯示調用代碼所需的依賴項、調用代碼的方式以及嘗試斷言的内容.

是以在進行單元測試的編寫的時候, 請使用注釋标記出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方法。

可以這樣認為: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

官方倉庫

  • https://github.com/moq/moq4

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

官方倉庫

  • https://github.com/AutoFixture/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版本,社群版不帶這個功能。

如何檢視覆寫率

  • 在測試視窗下,右鍵相應的測試組
  • 點選如下的"分析代碼覆寫率"
淺談.Net Core後端單元測試1. 前言2. 為什麼需要單元測試3. 基本原則和規範4. 常用類庫介紹5. 實踐中結合Visual Studio的使用6. 實踐中常見場景的Mock7. 拓展

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
淺談.Net Core後端單元測試1. 前言2. 為什麼需要單元測試3. 基本原則和規範4. 常用類庫介紹5. 實踐中結合Visual Studio的使用6. 實踐中常見場景的Mock7. 拓展

方法二(推薦)

使用現成的庫(也是基于上面的方式封裝好的)

倉庫位址:

  • https://github.com/romantitov/MockQueryable

使用範例

// 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扼殺在在搖籃狀态。

淺談.Net Core後端單元測試1. 前言2. 為什麼需要單元測試3. 基本原則和規範4. 常用類庫介紹5. 實踐中結合Visual Studio的使用6. 實踐中常見場景的Mock7. 拓展

此種開發模式以測試先行,對開發團隊的要求較高, 落地可能會存在很多實際困難。詳細說明可以參考如下

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

參考連結

  • 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

繼續閱讀