單元測試中的 MockManager - 用於模擬的構建器模式
幾年前我寫過有關此的文章,但不太詳細。這是同一想法的更精緻的版本。
簡介
單元測試對開發人員來說既是福也是禍。它們允許快速測試功能、可讀的使用示例、快速實驗所涉及組件的場景。但它們也可能變得混亂,需要在每次代碼更改時進行維護和更新,並且如果懶惰地完成,則無法隱藏錯誤而不是揭示錯誤。
我認為單元測試如此困難的原因是它與測試相關,而不是代碼編寫,而且單元測試的編寫方式與我們編寫的大多數其他代碼相反。
在這篇文章中,我將為您提供一種編寫單元測試的簡單模式,該模式將增強所有好處,同時消除與正常代碼的大部分認知失調。單元測試將保持可讀性和靈活性,同時減少重複代碼並且不添加額外的依賴項。
如何進行單元測試
但首先,讓我們定義一個好的單元測試套件。
要正確測試一個類,必須以某種方式編寫它。在這篇文章中,我們將介紹使用構造函數注入進行依賴項的類,這是我推薦的進行依賴項注入的方法。
然後,為了測試它,我們需要:
- 涵蓋積極的場景- 當類執行其應該執行的操作時,使用設置和輸入參數的各種組合來涵蓋整個功能
- 涵蓋負面場景- 當設置或輸入參數錯誤時,類以正確的方式失敗
- 模擬所有外部依賴
- 將所有測試設置、操作和斷言保留在同一個測試中(通常稱為arrange-act-assert 結構)
但這說起來容易做起來難,因為它還意味著:
- 為每個測試設置相同的依賴項,從而復制和粘貼大量代碼
- 設置非常相似的場景,兩次測試之間僅進行一次更改,再次重複大量代碼
- 什麼都不概括和封裝,這是開發人員通常在所有代碼中所做的事情
- 為很少的正例寫了很多負例,感覺就像測試代碼比功能代碼多
- 必須為測試類的每次更改更新所有這些測試
誰喜歡這個?
解決方案
解決方案是使用構建器軟件模式在arrange-act-assert 結構中創建流暢、靈活且可讀的測試,同時將設置代碼封裝在一個類中,以補充特定服務的單元測試套件。我稱之為mockmanager 模式。
讓我們從一個簡單的例子開始:
// the tested class public class calculator { private readonly itokenparser tokenparser; private readonly imathoperationfactory operationfactory; private readonly icache cache; private readonly ilogger logger; public calculator( itokenparser tokenparser, imathoperationfactory operationfactory, icache cache, ilogger logger) { this.tokenparser = tokenparser; this.operationfactory = operationfactory; this.cache = cache; this.logger = logger; } public int calculate(string input) { var result = cache.get(input); if (result.hasvalue) { logger.loginformation("from cache"); return result.value; } var tokens = tokenparser.parse(input); ioperation operation = null; foreach(var token in tokens) { if (operation is null) { operation = operationfactory.getoperation(token.operationtype); continue; } if (result is null) { result = token.value; continue; } else { if (result is null) { throw new invalidoperationexception("could not calculate result"); } result = operation.execute(result.value, token.value); operation = null; } } cache.set(input, result.value); logger.loginformation("from operation"); return result.value; } }
這是一個計算器,按照傳統。它接收一個字符串並返回一個整數值。它還緩存特定輸入的結果,並記錄一些內容。實際操作由imathoperationfactory 抽象,輸入字符串由itokenparser 轉換為標記。別擔心,這不是一個真正的課程,只是一個例子。讓我們看一個“傳統”測試:
[testmethod] public void calculate_additionworks() { // arrange var tokenparsermock = new mock<itokenparser>(); tokenparsermock .setup(m => m.parse(it.isany<string>())) .returns( new list<calculatortoken> { calculatortoken.addition, calculatortoken.from(1), calculatortoken.from(1) } ); var mathoperationfactorymock = new mock<imathoperationfactory>(); var operationmock = new mock<ioperation>(); operationmock .setup(m => m.execute(1, 1)) .returns(2); mathoperationfactorymock .setup(m => m.getoperation(operationtype.add)) .returns(operationmock.object); var cachemock = new mock<icache>(); var loggermock = new mock<ilogger>(); var service = new calculator( tokenparsermock.object, mathoperationfactorymock.object, cachemock.object, loggermock.object); // act service.calculate(""); //assert mathoperationfactorymock .verify(m => m.getoperation(operationtype.add), times.once); operationmock .verify(m => m.execute(1, 1), times.once); } </ilogger></icache></ioperation></imathoperationfactory></calculatortoken></string></itokenparser>
讓我們稍微打開一下它。例如,即使我們實際上並不關心記錄器或緩存,我們也必須為每個構造函數依賴項聲明一個模擬。在操作工廠的情況下,我們還必須設置一個返回另一個模擬的模擬方法。
在這個特定的測試中,我們主要編寫了設置、一行act 和兩行assert。此外,如果我們想測試緩存在類中的工作原理,我們必須複製粘貼整個內容,然後更改我們設置緩存模擬的方式。
還有一些負面測試需要考慮。我見過許多負面測試做了類似的事情:“設置應該失敗的內容。測試它失敗”,這引入了很多問題,主要是因為它可能會因完全不同的原因而失敗,並且大多數時候這些測試遵循類的內部實現而不是其要求。正確的陰性測試實際上是完全陽性的測試,只有一個錯誤的條件。為了簡單起見,這裡的情況並非如此。
所以,言歸正傳,這裡是相同的測試,但使用了mockmanager:
[testmethod] public void calculate_additionworks_mockmanager() { // arrange var mockmanager = new calculatormockmanager() .withparsedtokens(new list<calculatortoken> { calculatortoken.addition, calculatortoken.from(1), calculatortoken.from(1) }) .withoperation(operationtype.add, 1, 1, 2); var service = mockmanager.getservice(); // act service.calculate(""); //assert mockmanager .verifyoperationexecute(operationtype.add, 1, 1, times.once); } </calculatortoken>
拆包,沒有提到緩存或記錄器,因為我們不需要在那裡進行任何設置。一切都已打包且可讀。複製粘貼此內容並更改一些參數或某些行不再難看。在arrange 中執行了三種方法,一種在act 中執行,一種在assert 中執行。僅抽象了實質的模擬細節:這裡沒有提及moq 框架。事實上,無論決定使用哪種模擬框架,此測試看起來都是一樣的。
讓我們看一下mockmanager 類。現在這會顯得很複雜,但請記住,我們只編寫一次並使用它很多次。該類的整體複雜性是為了使單元測試易於人類閱讀,易於理解、更新和維護。
public class CalculatorMockManager { private readonly Dictionary<operationtype>> operationMocks = new(); public Mock<itokenparser> TokenParserMock { get; } = new(); public Mock<imathoperationfactory> MathOperationFactoryMock { get; } = new(); public Mock<icache> CacheMock { get; } = new(); public Mock<ilogger> LoggerMock { get; } = new(); public CalculatorMockManager WithParsedTokens(List<calculatortoken> tokens) { TokenParserMock .Setup(m => m.Parse(It.IsAny<string>())) .Returns( new List<calculatortoken> { CalculatorToken.Addition, CalculatorToken.From(1), CalculatorToken.From(1) } ); return this; } public CalculatorMockManager WithOperation(OperationType operationType, int v1, int v2, int result) { var operationMock = new Mock<ioperation>(); operationMock .Setup(m => m.Execute(v1, v2)) .Returns(result); MathOperationFactoryMock .Setup(m => m.GetOperation(operationType)) .Returns(operationMock.Object); operationMocks[operationType] = operationMock; return this; } public Calculator GetService() { return new Calculator( TokenParserMock.Object, MathOperationFactoryMock.Object, CacheMock.Object, LoggerMock.Object ); } public CalculatorMockManager VerifyOperationExecute(OperationType operationType, int v1, int v2, Func<times> times) { MathOperationFactoryMock .Verify(m => m.GetOperation(operationType), Times.AtLeastOnce); var operationMock = operationMocks[operationType]; operationMock .Verify(m => m.Execute(v1, v2), times); return this; } } </times></ioperation></calculatortoken></string></calculatortoken></ilogger></icache></imathoperationfactory></itokenparser></operationtype>
測試類所需的所有模擬都被聲明為公共屬性,允許對單元測試進行任何自定義。有一個getservice 方法,它將始終返回被測試類的實例,並且所有依賴項都完全模擬。然後還有with* 方法,它們自動設置各種場景並始終返回模擬管理器,以便可以鏈接它們。您還可以使用特定的斷言方法,儘管在大多數情況下您會將一些輸出與預期值進行比較,因此這些只是為了抽像出moq 框架的verify 方法。
結論
此模式現在使測試編寫與代碼編寫保持一致:
- 抽像出任何上下文中你不關心的事物
- 一次編寫,多次使用
- 人類可讀的自記錄代碼
- 低圈複雜度的小方法
- 直觀的代碼編寫
現在編寫單元測試既簡單又一致:
- 實例化您要測試的類的模擬管理器(或根據上述步驟編寫一個)
- 為測試編寫特定場景(自動完成現有已涵蓋的場景步驟)
- 使用測試參數執行你想要測試的方法
- 檢查一切是否符合預期
抽象並不止於模擬框架。相同的模式可以應用於每種編程語言!對於typescript 或javascript 或其他東西來說,模擬管理器構造將非常不同,但單元測試看起來幾乎是一樣的。
希望這有幫助!
以上是單元測試中的 MockManager - 用於模擬的構建器模式的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

JavaScript 不提供任何記憶體管理操作。相反,記憶體由 JavaScript VM 透過記憶體回收過程管理,該過程稱為垃圾收集。

問題:Vue3+TypeScript+Vite的專案中如何使用require動態引入類似於圖片等靜態資源!描述:今天在開發專案時(專案框架為Vue3+TypeScript+Vite)需要動態引入靜態資源,也就是img標籤的src屬性值為動態獲取,按照以往的做法直接是require引入即可,如下程式碼:寫上後程式碼波浪線報錯,報錯提示:找不到名稱「require」。是否需要為節點安裝類型定義?請嘗試使用npmi--save-dev@types/node。 ts(2580)在進行了npmi--save-d

如何使用MySQL在TypeScript中實作資料型別轉換功能引言:在開發Web應用程式時,資料型別轉換是一個非常常見的需求。在處理資料庫中儲存的資料時,特別是使用MySQL作為後端資料庫時,我們經常需要將查詢結果中的資料按照我們所需的類型進行轉換。本文將介紹如何在TypeScript中利用MySQL實作資料類型轉換的功能,並提供程式碼範例。一、準備工作:在開

如何使用Redis和TypeScript開發高效能運算功能概述:Redis是一個開源的記憶體資料結構儲存系統,具有高效能和可擴展性的特性。 TypeScript是JavaScript的超集,提供了型別系統和更好的開發工具支援。結合Redis和TypeScript,我們可以開發出高效的運算功能來處理大數據集,並充分利用Redis的記憶體儲存和運算能力。本文將介紹如何

如何聲明字段名為枚舉的類型?根據設計,type欄位應該是一個列舉值,不應該由呼叫方隨意設定。下面是Type的枚舉聲明,共有6個欄位。 enumType{primary="primary",success="success",warning="warning",warn="warn",//warningaliasdanger="danger",info="info",}TypeSc

Vue3相較於Vue2的變化:更好的TypeScript類型推導Vue是一種流行的JavaScript框架,用於建立使用者介面。而Vue3是Vue框架的最新版本,在Vue2的基礎上進行了大量改進和最佳化。其中之一是在TypeScript類型推導方面的提升。本文將介紹Vue3在類型推導方面的改進,並且透過程式碼範例進行說明。在Vue2中,我們需要手動為Vue組件

標題:使用Redis和TypeScript開發可擴展的前端應用程式引言:在當今互聯網時代,可擴展性是任何應用程式的關鍵要素之一。前端應用程式也不例外。為了滿足用戶日益增長的需求,我們需要使用高效可靠的技術來建立可擴展的前端應用程式。在本文中,我們將介紹如何使用Redis和TypeScript來開發可擴展的前端應用程序,並透過程式碼範例示範其應用。 Redis簡介

隨著JavaScript的不斷發展,前端工程師們已經逐漸意識到JavaScript本身存在的一些問題,例如缺乏型別檢查和模組化,這些問題在大型專案中經常會造成混亂和錯誤。為了解決這些問題,TypeScript應運而生,成為前端開發中越來越受歡迎的語言。而在後端開發領域中,PHP一直是極為流行的腳本語言。因此,結合TypeScript來開發PHP的應用程式
