淺談.Net Core后端單元測(cè)試的實(shí)現(xiàn)
1. 前言
單元測(cè)試一直都是"好處大家都知道很多,但是因?yàn)榉N種原因沒有實(shí)施起來(lái)"的一個(gè)老大難問(wèn)題。具體是否應(yīng)該落地單元測(cè)試,以及落地的程度, 每個(gè)項(xiàng)目都有自己的情況。
本篇為個(gè)人認(rèn)為"如何更好地寫單元測(cè)試", 即更加 偏向?qū)嵺`向 中夾雜一些理論的分享。
下列示例的單元測(cè)試框架為 xUnit
, Mock庫(kù)為 Moq
2. 為什么需要單元測(cè)試
優(yōu)點(diǎn)有很多, 這里提兩點(diǎn)我個(gè)人認(rèn)為的很明顯的好處
2.1 防止回歸
通常在進(jìn)行新功能/模塊的開發(fā)或者是重構(gòu)的時(shí)候,測(cè)試會(huì)進(jìn)行回歸測(cè)試原有的已存在的功能,以驗(yàn)證以前實(shí)現(xiàn)的功能是否仍能按預(yù)期運(yùn)行。
使用單元測(cè)試,可在每次生成后,甚至在更改一行代碼后重新運(yùn)行整套測(cè)試, 從而可以很大程度減少回歸缺陷。
2.2 減少代碼耦合
當(dāng)代碼緊密耦合或者一個(gè)方法過(guò)長(zhǎng)的時(shí)候,編寫單元測(cè)試會(huì)變得很困難。當(dāng)不去做單元測(cè)試的時(shí)候,可能代碼的耦合不會(huì)給人感覺那么明顯。為代碼編寫測(cè)試會(huì)自然地解耦代碼,變相提高代碼質(zhì)量和可維護(hù)性。
3. 基本原則和規(guī)范
3.1 3A原則
3A分別是"arrange、act、assert", 分別代表一個(gè)合格的單元測(cè)試方法的三個(gè)階段
- 事先的準(zhǔn)備
- 測(cè)試方法的實(shí)際調(diào)用
- 針對(duì)返回值的斷言
一個(gè)單元測(cè)試方法可讀性是編寫測(cè)試時(shí)最重要的方面之一。 在測(cè)試中分離這些操作會(huì)明確地突出顯示調(diào)用代碼所需的依賴項(xiàng)、調(diào)用代碼的方式以及嘗試斷言的內(nèi)容.
所以在進(jìn)行單元測(cè)試的編寫的時(shí)候, 請(qǐng)使用注釋標(biāo)記出3A的各個(gè)階段的, 如下示例
[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 盡量避免直接測(cè)試私有方法
盡管私有方法可以通過(guò)反射進(jìn)行直接測(cè)試,但是在大多數(shù)情況下,不需要直接測(cè)試私有的private方法, 而是通過(guò)測(cè)試公共public方法來(lái)驗(yàn)證私有的private方法。
可以這樣認(rèn)為:private方法永遠(yuǎn)不會(huì)孤立存在。更應(yīng)該關(guān)心的是調(diào)用private方法的public方法的最終結(jié)果。
3.3 重構(gòu)原則
如果一個(gè)類/方法,有很多的外部依賴,造成單元測(cè)試的編寫困難。那么應(yīng)該考慮當(dāng)前的設(shè)計(jì)和依賴項(xiàng)是否合理。是否有部分可以存在解耦的可能性。選擇性重構(gòu)原有的方法,而不是硬著頭皮寫下去.
3.4 避免多個(gè)斷言
如果一個(gè)測(cè)試方法存在多個(gè)斷言,可能會(huì)出現(xiàn)某一個(gè)或幾個(gè)斷言失敗導(dǎo)致整個(gè)方法失敗。這樣不能從根本上知道是了解測(cè)試失敗的原因。
所以一般有兩種解決方案
- 拆分成多個(gè)測(cè)試方法
- 使用參數(shù)化測(cè)試, 如下示例
[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); }
當(dāng)然如果是對(duì)對(duì)象進(jìn)行斷言, 可能會(huì)對(duì)對(duì)象的多個(gè)屬性都有斷言。此為例外。
3.5 文件和方法命名規(guī)范 文件名規(guī)范
一般有兩種。比如針對(duì) UserController
下方法的單元測(cè)試應(yīng)該統(tǒng)一放在 UserControllerTest
或者 UserController_Test
下
單元測(cè)試方法名
單元測(cè)試的方法名應(yīng)該具有可讀性,讓整個(gè)測(cè)試方法在不需要注釋說(shuō)明的情況下可以被讀懂。格式應(yīng)該類似遵守如下
<被測(cè)試方法全名>_<期望的結(jié)果>_<給予的條件> // 例子 [Fact] public void Add_InputNullOrAlphabetic_ThrowsArgumentException() { ... }
4. 常用類庫(kù)介紹
4.1 xUnit/MsTest/NUnit
編寫.Net Core的單元測(cè)試?yán)@不過(guò)要選擇一個(gè)單元測(cè)試的框架, 三大單元測(cè)試框架中
- MsTest是微軟官方出品的一個(gè)測(cè)試框架
- NUnit沒用過(guò)
- xUnit是.Net Foundation下的一個(gè)開源項(xiàng)目,并且被dotnet github上很多倉(cāng)庫(kù)(包括runtime)使用的單元測(cè)試框架
三大測(cè)試框架發(fā)展至今已是大差不差, 很多時(shí)候選擇只是靠個(gè)人的喜好。
個(gè)人偏好 xUnit
簡(jiǎn)潔的斷言
// xUnit Assert.True() Assert.Equal() // MsTest Assert.IsTrue() Assert.AreEqual()
客觀地功能性地分析三大框架地差異可以參考如下
https://anarsolutions.com/automated-unit-testing-tools-comparison
4.2 Moq
官方倉(cāng)庫(kù)
Moq是一個(gè)非常流行的模擬庫(kù), 只要有一個(gè)接口它就可以動(dòng)態(tài)生成一個(gè)對(duì)象, 底層使用的是Castle的動(dòng)態(tài)代理功能.
基本用法
在實(shí)際使用中可能會(huì)有如下場(chǎng)景
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 { ... } } }
在進(jìn)行單元測(cè)試的時(shí)候, 可以使用 Moq
對(duì) _userService.GetUser
進(jìn)行模擬返回值
[Fact] public void GetUser_ShouldReturnNotFound_WhenCannotFoundUser() { // arrange // 新建一個(gè)IUserService的mock對(duì)象 var mockUserService = new Mock<IUserService>(); // 使用moq對(duì)IUserService的GetUs方法進(jìn)行mock: 當(dāng)入?yún)?33時(shí)返回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 // 驗(yàn)證調(diào)用過(guò)userService的GetUser方法一次,且入?yún)?33 mockUserService.Verify(it => it.GetUser(233), Times.AtMostOnce()); }
4.3 AutoFixture
官方倉(cāng)庫(kù)
https://github.com/AutoFixture/AutoFixture
AutoFixture是一個(gè)假數(shù)據(jù)填充庫(kù),旨在最小化3A中的 arrange
階段,使開發(fā)人員更容易創(chuàng)建包含測(cè)試數(shù)據(jù)的對(duì)象,從而可以更專注與測(cè)試用例的設(shè)計(jì)本身。
基本用法
直接使用如下的方式創(chuàng)建強(qiáng)類型的假數(shù)據(jù)
[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); }
上述示例也可以和測(cè)試框架本身結(jié)合,比如xUnit
[Theory, AutoData] public void IntroductoryTest( int expectedNumber, MyClass sut) { // act int result = sut.Echo(expectedNumber); // assert Assert.Equal(expectedNumber, result); }
5. 實(shí)踐中結(jié)合Visual Studio的使用
Visual Studio提供了完備的單元測(cè)試的支持,包括運(yùn)行. 編寫. 調(diào)試單元測(cè)試。以及查看單元測(cè)試覆蓋率等。
5.1 如何在Visual Studio中運(yùn)行單元測(cè)試
5.2 如何在Visual Studio中查看單元測(cè)試覆蓋率
如下功能需要Visual Studio 2019 Enterprise版本,社區(qū)版不帶這個(gè)功能。
如何查看覆蓋率
- 在測(cè)試窗口下,右鍵相應(yīng)的測(cè)試組 點(diǎn)
- 點(diǎn)擊如下的"分析代碼覆蓋率"
6. 實(shí)踐中常見場(chǎng)景的Mock
主要
6.1 DbSet
使用EF Core過(guò)程中,如何mock DbSet是一個(gè)繞不過(guò)的坎。
方法一
參考如下鏈接的回答進(jìn)行自行封裝
https://stackoverflow.com/questions/31349351/how-to-add-an-item-to-a-mock-dbset-using-moq
方法二(推薦)
使用現(xiàn)成的庫(kù)(也是基于上面的方式封裝好的)
倉(cāng)庫(kù)地址:
https://github.com/romantitov/MockQueryable
使用范例
// 1. 測(cè)試時(shí)創(chuàng)建一個(gè)模擬的List<T> var users = new List<UserEntity>() { new UserEntity{LastName = "ExistLastName", DateOfBirth = DateTime.Parse("01/20/2012")}, ... }; // 2. 通過(guò)擴(kuò)展方法轉(zhuǎn)換成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的場(chǎng)景
如果使用的是 RestEase
或者 Refit
等第三方庫(kù),具體接口的定義本質(zhì)上就是一個(gè)interface,所以直接使用moq進(jìn)行方法mock即可。
并且建議使用這種方式。
IHttpClientFactory
如果使用的是.Net Core自帶的 IHttpClientFactory
方式來(lái)請(qǐng)求外部接口的話,可以參考如下的方式對(duì) IHttpClientFactory
進(jìn)行mock
https://www.thecodebuzz.com/unit-test-mock-httpclientfactory-moq-net-core/
6.3 ILogger
由于ILogger的LogError等方法都是屬于擴(kuò)展方法,所以不需要特別的進(jìn)行方法級(jí)別的mock。
針對(duì)平時(shí)的一些使用場(chǎng)景封裝了一個(gè)幫助類, 可以使用如下的幫助類進(jìn)行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是測(cè)試驅(qū)動(dòng)開發(fā)(Test-Driven Development)的英文簡(jiǎn)稱. 一般是先提前設(shè)計(jì)好單元測(cè)試的各種場(chǎng)景再進(jìn)行真實(shí)業(yè)務(wù)代碼的編寫,編織安全網(wǎng)以便將Bug扼殺在在搖籃狀態(tài)。
此種開發(fā)模式以測(cè)試先行,對(duì)開發(fā)團(tuán)隊(duì)的要求較高, 落地可能會(huì)存在很多實(shí)際困難。詳細(xì)說(shuō)明可以參考如下
https://www.guru99.com/test-driven-development.html
參考鏈接
https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices
https://github.com/AutoFixture/AutoFixture
到此這篇關(guān)于淺談.Net Core后端單元測(cè)試的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān).Net Core 單元測(cè)試內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- .NET Core單元測(cè)試的兩種方法介紹
- ASP.NET?Core項(xiàng)目使用xUnit進(jìn)行單元測(cè)試
- ASP.NET Core對(duì)Controller進(jìn)行單元測(cè)試的完整步驟
- xUnit 編寫 ASP.NET Core 單元測(cè)試的方法
- ASP.NET Core針對(duì)一個(gè)使用HttpClient對(duì)象的類編寫單元測(cè)試詳解
- 詳解.Net單元測(cè)試方法
- ASP.NET Core中使用xUnit進(jìn)行單元測(cè)試
- .NET單元測(cè)試使用AutoFixture按需填充的幾種方式和最佳實(shí)踐記錄
相關(guān)文章
.net 中的 StringBuilder 和 TextWriter 區(qū)別詳解
這篇文章主要介紹了.net 中的 StringBuilder 和 TextWriter 區(qū)別詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09.net jMail郵件發(fā)送(含抄送、密送、多發(fā)、日志記錄)實(shí)例代碼
這篇文章主要介紹了.net jMail郵件發(fā)送(含抄送、密送、多發(fā)、日志記錄)實(shí)例代碼,有需要的朋友可以參考一下2013-11-11ASP.NET中CKEditor與CKFinder的配置使用
這篇文章主要介紹了ASP.NET中CKEditor與CKFinder的配置使用的相關(guān)資料,需要的朋友可以參考下2015-06-06ASP.NET小結(jié)之MVC, MVP, MVVM比較以及區(qū)別(一)
MVC, MVP和MVVM都是用來(lái)解決界面呈現(xiàn)和邏輯代碼分離而出現(xiàn)的模式。以前只是對(duì)它們有部分的了解,沒有深入的研究過(guò),對(duì)于一些里面的概念和區(qū)別也是一知半解。現(xiàn)在一邊查資料,并結(jié)合自己的理解,來(lái)談一下對(duì)于這三種模式思想的理解,以及它們的區(qū)別。歡迎各位高手拍磚。2014-05-05超好用輕量級(jí)MVC分頁(yè)控件JPager.Net
本文給大家分享的是一款超好用輕量級(jí)MVC分頁(yè)控件--JPager.Net,小編自己也在使用,非常的不錯(cuò),推薦給大家。2016-06-06