淺談.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

參考連接

相關文章
相關標籤/搜索