


MockManager in Unit-Tests – ein Builder-Muster, das für Mocks verwendet wird
Dec 19, 2024 pm 12:27 PMVor ein paar Jahren habe ich darüber geschrieben, allerdings weniger ausführlich. Hier ist eine verfeinerte Version derselben Idee.
Einführung
Unit-Tests sind für Entwickler sowohl Segen als auch Fluch. Sie erm?glichen ein schnelles Testen der Funktionalit?t, lesbare Anwendungsbeispiele und ein schnelles Experimentieren von Szenarien nur für die beteiligten Komponenten. Aber sie k?nnen auch chaotisch werden, müssen bei jeder Code?nderung gewartet und aktualisiert werden und k?nnen, wenn sie tr?ge gemacht werden, Fehler nicht eher verbergen, als dass sie aufgedeckt werden.
Ich denke, der Grund dafür, dass Unit-Tests so schwierig sind, liegt darin, dass sie mit Testen verbunden sind, mit etwas anderem als dem Schreiben von Code, und au?erdem darin, dass Unit-Tests anders geschrieben werden als die meisten anderen Codes, die wir schreiben.
In diesem Beitrag werde ich Ihnen ein einfaches Muster zum Schreiben von Unit-Tests vorstellen, das alle Vorteile hervorhebt und gleichzeitig die meisten kognitiven Dissonanzen mit normalem Code beseitigt. Unit-Tests bleiben lesbar und flexibel, w?hrend doppelter Code reduziert wird und keine zus?tzlichen Abh?ngigkeiten hinzugefügt werden.
So führen Sie einen Unit-Test durch
Aber zuerst definieren wir eine gute Unit-Test-Suite.
Um eine Klasse richtig zu testen, muss sie auf eine bestimmte Art und Weise geschrieben sein. In diesem Beitrag werden wir Klassen behandeln, die die Konstruktorinjektion für Abh?ngigkeiten verwenden, was meine empfohlene Methode zur Abh?ngigkeitsinjektion ist.
Um es dann zu testen, müssen wir Folgendes tun:
- Positive Szenarien abdecken – wenn die Klasse das tut, was sie tun soll, mit verschiedenen Kombinationen von Setup- und Eingabeparametern, um die gesamte Funktionalit?t abzudecken
- Negative Szenarien abdecken – wenn die Klasse auf die richtige Weise fehlschl?gt, wenn die Einrichtungs- oder Eingabeparameter falsch sind
- alle externen Abh?ngigkeiten verspotten
- Behalten Sie den gesamten Testaufbau, die Aktion und die Behauptung im selben Test bei (was normalerweise als Arrange-Act-Assert-Struktur bezeichnet wird)
Aber das ist leichter gesagt als getan, denn es impliziert auch:
- Einrichten der gleichen Abh?ngigkeiten für jeden Test, wodurch viel Code kopiert und eingefügt werden muss
- Einrichten sehr ?hnlicher Szenarien, mit nur einer ?nderung zwischen zwei Tests und erneuter Wiederholung einer Menge Code
- nichts verallgemeinern und kapseln, was ein Entwickler normalerweise in seinem gesamten Code tun würde
- Viele negative F?lle für wenige positive F?lle schreiben, was sich anfühlt, als müsste man mehr Code als Funktionscode testen
- alle diese Tests müssen für jede ?nderung an der getesteten Klasse aktualisiert werden
Wer liebt das?
L?sung
Die L?sung besteht darin, das Builder-Softwaremuster zu verwenden, um flüssige, flexible und lesbare Tests in der Arrange-Act-Assert-Struktur zu erstellen und gleichzeitig Setup-Code in einer Klasse zu kapseln, die die Unit-Test-Suite für einen bestimmten Dienst erg?nzt. Ich nenne das das MockManager-Muster.
Beginnen wir mit einem einfachen Beispiel:
// 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; } }
Dies ist traditionell ein Taschenrechner. Es empf?ngt eine Zeichenfolge und gibt einen ganzzahligen Wert zurück. Au?erdem wird das Ergebnis für eine bestimmte Eingabe zwischengespeichert und einige Dinge protokolliert. Die eigentlichen Operationen werden von IMathOperationFactory abstrahiert und die Eingabezeichenfolge wird von einem ITokenParser in Token übersetzt. Keine Sorge, dies ist keine echte Klasse, sondern nur ein Beispiel. Schauen wir uns einen ?traditionellen“ Test an:
[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); }
Lass es uns ein wenig auspacken. Wir mussten für jede Konstruktorabh?ngigkeit einen Mock deklarieren, auch wenn uns beispielsweise der Logger oder der Cache eigentlich egal sind. Wir mussten auch eine Mock-Methode einrichten, die im Fall der Operation Factory einen anderen Mock zurückgibt.
In diesem speziellen Test haben wir haupts?chlich Setup geschrieben, eine Zeile Act und zwei Zeilen Assert. Wenn wir au?erdem testen m?chten, wie der Cache innerhalb der Klasse funktioniert, müssten wir das Ganze kopieren und einfügen und einfach die Art und Weise ?ndern, wie wir den Cache-Mock einrichten.
Und da sind noch die negativen Tests zu berücksichtigen. Ich habe viele negative Tests gesehen, die so etwas machten wie: ?Einrichten, was fehlschlagen soll. Testen, dass es fehlschl?gt“, was viele Probleme mit sich bringt, vor allem, weil es aus v?llig anderen Gründen fehlschlagen kann und meistens diese Tests folgen eher der internen Implementierung der Klasse als ihren Anforderungen. Ein ordnungsgem?? negativer Test ist eigentlich ein vollst?ndig positiver Test mit nur einer falschen Bedingung. Der Einfachheit halber ist dies hier nicht der Fall.
Also, ohne weitere Umschweife, hier ist derselbe Test, aber mit einem 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); }
Beim Auspacken wird weder Cache noch Logger erw?hnt, da wir dort keine Einrichtung ben?tigen. Alles ist verpackt und lesbar. Das Kopieren, Einfügen und ?ndern einiger Parameter oder Zeilen ist nicht mehr h?sslich. In Arrange werden drei Methoden ausgeführt, eine in Act und eine in Assert. Es werden nur die kleinsten sp?ttischen Details abstrahiert: Das Moq-Framework wird hier nicht erw?hnt. Tats?chlich würde dieser Test gleich aussehen, unabh?ngig davon, für welches Spott-Framework man sich entscheidet.
Werfen wir einen Blick auf die MockManager-Klasse. Das mag zwar kompliziert erscheinen, aber denken Sie daran, dass wir es nur einmal schreiben und es viele Male verwenden. Die ganze Komplexit?t der Klasse dient dazu, Unit-Tests für Menschen lesbar, leicht zu verstehen, zu aktualisieren und zu warten.
public class CalculatorMockManager { private readonly Dictionary<OperationType,Mock<IOperation>> 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; } }
Alle erforderlichen Mocks für die Testklasse werden als ?ffentliche Eigenschaften deklariert, sodass ein Komponententest beliebig angepasst werden kann. Es gibt eine GetService-Methode, die immer eine Instanz der getesteten Klasse zurückgibt, wobei alle Abh?ngigkeiten vollst?ndig simuliert sind. Dann gibt es With*-Methoden, die verschiedene Szenarien atomar einrichten und immer den Mock-Manager zurückgeben, sodass sie verkettet werden k?nnen. Sie k?nnen auch bestimmte Methoden zur Best?tigung verwenden, obwohl Sie in den meisten F?llen eine Ausgabe mit einem erwarteten Wert vergleichen werden. Diese dienen also nur dazu, die Verify-Methode des Moq-Frameworks zu abstrahieren.
Abschluss
Dieses Muster gleicht nun das Schreiben von Tests mit dem Schreiben von Code aus:
- Abstrahieren Sie die Dinge, die Ihnen in keinem Kontext wichtig sind
- Einmal schreiben und mehrmals verwenden
- Menschenlesbarer, selbstdokumentierender Code
- kleine Methoden mit geringer zyklomatischer Komplexit?t
- Intuitives Schreiben von Code
Jetzt einen Unit-Test zu schreiben ist trivial und konsistent:
- Instanziieren Sie den Mock-Manager der Klasse, die Sie testen m?chten (oder schreiben Sie einen basierend auf den obigen Schritten)
- Erstellen Sie spezifische Szenarien für den Test (mit automatischer Vervollst?ndigung für vorhandene, bereits abgedeckte Szenarioschritte)
- Führen Sie die Methode aus, die Sie testen m?chten, mit Testparametern
- überprüfen Sie, ob alles wie erwartet ist
Die Abstraktion h?rt nicht beim sp?ttischen Rahmen auf. Das gleiche Muster kann in jeder Programmiersprache angewendet werden! Das Mock-Manager-Konstrukt wird für TypeScript, JavaScript oder etwas anderes sehr unterschiedlich sein, aber der Unit-Test würde ziemlich gleich aussehen.
Ich hoffe, das hilft!
Das obige ist der detaillierte Inhalt vonMockManager in Unit-Tests – ein Builder-Muster, das für Mocks verwendet wird. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Hei?e KI -Werkzeuge

Undress AI Tool
Ausziehbilder kostenlos

Undresser.AI Undress
KI-gestützte App zum Erstellen realistischer Aktfotos

AI Clothes Remover
Online-KI-Tool zum Entfernen von Kleidung aus Fotos.

Clothoff.io
KI-Kleiderentferner

Video Face Swap
Tauschen Sie Gesichter in jedem Video mühelos mit unserem v?llig kostenlosen KI-Gesichtstausch-Tool aus!

Hei?er Artikel

Hei?e Werkzeuge

Notepad++7.3.1
Einfach zu bedienender und kostenloser Code-Editor

SublimeText3 chinesische Version
Chinesische Version, sehr einfach zu bedienen

Senden Sie Studio 13.0.1
Leistungsstarke integrierte PHP-Entwicklungsumgebung

Dreamweaver CS6
Visuelle Webentwicklungstools

SublimeText3 Mac-Version
Codebearbeitungssoftware auf Gottesniveau (SublimeText3)

Hei?e Themen

Ja, die überlastung von Funktionen ist eine polymorphe Form in C, speziell kompiliert-Time-Polymorphismus. 1. Funktionsüberladung erm?glicht mehrere Funktionen mit demselben Namen, aber unterschiedlichen Parameterlisten. 2. Der Compiler entscheidet, welche Funktion zur Kompilierung der entsprechenden Parameter zu Kompilierzeit aufgerufen werden soll. 3. Im Gegensatz zum Laufzeitpolymorphismus hat Funktion überladung zur Laufzeit keinen zus?tzlichen Overhead und ist einfach zu implementieren, aber weniger flexibel.

C hat zwei polymorphe Haupttypen: Kompilierungszeitpolymorphismus und Laufzeitpolymorphismus. 1. Die Kompilierungszeitpolymorphismus wird durch Funktion überladung und Vorlagen implementiert, was eine hohe Effizienz bietet, kann jedoch zu Code-Bl?hungen führen. 2. Die Laufzeitpolymorphismus wird durch virtuelle Funktionen und Vererbung implementiert, die Flexibilit?t, aber Leistungsaufwand bietet.

Ja, Polymorphismen in C sind sehr nützlich. 1) Es bietet Flexibilit?t, um eine einfache Erg?nzung neuer Typen zu erm?glichen. 2) f?rdert die Wiederverwendung von Code und reduziert die Duplikation; 3) vereinfacht die Wartung und erleichtert den Code, sich zu erweitern und sich an ?nderungen anzupassen. Trotz der Herausforderungen des Leistungs- und Ged?chtnismanagements sind die Vorteile in komplexen Systemen besonders von Bedeutung.

C DestructorscanleadtoseveralcommonErrors.Toavoidthem: 1) PREVORDDoUbledelTionBysettingPointerstonullPtrorusingsMartPointers.2) Handlexzepionsindrute -byCatchingandLoggingThem.3) UseVirirtualDestructorsinbaseClaStroperPoperPolymorpicdestruction.4

Polymorphismen in C werden in Laufzeitpolymorphismen und Kompilierungs-Zeit-Polymorphismen unterteilt. 1. Die Laufzeit -Polymorphismus wird durch virtuelle Funktionen implementiert, sodass die richtige Methode zur Laufzeit dynamisch aufgerufen werden kann. 2. Die Kompilierungszeitpolymorphismus wird durch Funktionsüberlastung und Vorlagen implementiert, wodurch eine h?here Leistung und Flexibilit?t erzielt wird.

Menschen, die den Python -Transfer zu C studieren. Die direkteste Verwirrung ist: Warum k?nnen Sie nicht wie Python schreiben? Da C, obwohl die Syntax komplexer ist, zugrunde liegenden Kontrollfunktionen und Leistungsvorteile. 1. In Bezug auf die Syntaxstruktur verwendet C Curly -Klammern {} anstelle von Einrückungen, um Codebl?cke zu organisieren, und variable Typen müssen explizit deklariert werden; 2. In Bezug auf das Typensystem und die Speicherverwaltung verfügt C nicht über einen automatischen Mülleimermechanismus und muss den Speicher manuell verwalten und auf die Freigabe von Ressourcen achten. Die Raii -Technologie kann das Ressourcenmanagement unterstützen. 3. In Funktionen und Klassendefinitionen muss C explizit auf Modifikatoren, Konstrukteure und Zerst?rer zugreifen und erweiterte Funktionen wie die überlastung des Bedieners unterstützen. 4. In Bezug auf Standardbibliotheken bietet STL leistungsstarke Container und Algorithmen, muss sich jedoch an generische Programmierideen anpassen. 5

C Polymorphismincludes-Compile-Time, Laufzeit und TemplatePolymorphismus.1) Compile-TimepolymorphismusseFranction undoperatoroverloading Forefficiency.2) RunTimepolymorphismPirtualFunctionsforflexibilit?t.3) templatepolymorphisMenenericProgrammprogrammen

C polymorphismisuniqueduetoitsCombinationofcompile-Timeandruntimepolymorphismus, der Forbothefficiency-Flexibilit?t erlaubt
