数年前、私はこれについて書きましたが、それほど詳しくはありませんでした。同じアイデアのさらに洗練されたバージョンを次に示します。
単体テストは開発者にとって恩恵でもあり、害でもあります。これらにより、機能の迅速なテスト、わかりやすい使用例、関係するコンポーネントのみのシナリオの迅速な実験が可能になります。しかし、煩雑になる可能性があり、コードを変更するたびにメンテナンスと更新が必要になり、怠惰に実行すると、バグを明らかにするどころか隠すことができなくなります。
単体テストが非常に難しい理由は、単体テストがコード作成以外のテストに関連していることと、単体テストが私たちが作成する他のほとんどのコードとは逆の方法で作成されるためだと思います。
この投稿では、通常のコードとの認知的不協和のほとんどを排除しながら、すべての利点を強化する単体テストを作成する簡単なパターンを紹介します。単体テストは可読性と柔軟性を維持しながら、重複コードを減らし、余分な依存関係を追加しません。
しかし、その前に、優れた単体テスト スイートを定義しましょう。
クラスを適切にテストするには、クラスを特定の方法で記述する必要があります。この投稿では、依存関係のコンストラクター注入を使用するクラスについて説明します。これは、依存関係の注入を行う私が推奨する方法です。
次に、それをテストするには、次のことを行う必要があります:
しかし、これは言うは易く行うは難し、次のことも意味するからです。
それが好きな人はいるでしょうか?
解決策は、ビルダー ソフトウェア パターンを使用して、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); }
少し開梱してみましょう。たとえば、実際にはロガーやキャッシュを気にしない場合でも、コンストラクターの依存関係ごとにモックを宣言する必要がありました。オペレーション ファクトリの場合、別のモックを返すモック メソッドも設定する必要がありました。
この特定のテストでは、セットアップの大部分、Act の 1 行と Assert の 2 行を書きました。さらに、クラス内でキャッシュがどのように機能するかをテストしたい場合は、全体をコピーして貼り付け、キャッシュ モックの設定方法を変更するだけで済みます。
そして、検討すべき陰性検査もあります。私は、「失敗するはずのものだけをセットアップし、失敗することをテストする」というようなことを行う否定的なテストを多く見てきました。これは多くの問題を引き起こします。主な理由は、まったく異なる理由で失敗する可能性があり、ほとんどの場合、これらのテストが失敗するためです。クラスの要件ではなく、クラスの内部実装に従っています。適切な陰性テストは、実際には、条件が 1 つだけ間違っているだけで完全に陽性となるテストです。簡単にするために、ここでは当てはまりません。
それでは、これ以上苦労せずに、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); }
解凍では、セットアップが必要ないため、キャッシュやロガーについては言及されていません。全てが詰まっていて読み応えがあります。これをコピーして貼り付け、いくつかのパラメーターや行を変更することは、もはや醜いものではありません。 Arrange では 3 つのメソッドが実行され、1 つは Act で、もう 1 つは Assert で実行されます。核心的なモックの詳細のみが抽象化されています。ここでは Moq フレームワークについては言及されていません。実際、このテストは、使用するモック フレームワークに関係なく、同じように見えます。
MockManager クラスを見てみましょう。これは複雑に見えますが、これを一度書くだけで何度も使用することに注意してください。クラス全体の複雑さは、単体テストを人間が読みやすく、理解し、更新、保守しやすいものにするためにあります。
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; } }
テスト クラスに必要なモックはすべてパブリック プロパティとして宣言されているため、単体テストをカスタマイズできます。 GetService メソッドがあり、すべての依存関係が完全にモック化された、テストされたクラスのインスタンスを常に返します。次に、さまざまなシナリオをアトミックに設定し、常にモック マネージャーを返す With* メソッドがあるため、チェーンできるようになります。アサーション用の特定のメソッドを使用することもできますが、ほとんどの場合、出力を期待値と比較することになるため、これらは Moq フレームワークの Verify メソッドを抽象化するためだけにここにあります。
このパターンは、テストの記述とコードの記述を調整するようになりました:
今単体テストを書くのは簡単で一貫性があります:
抽象化はモックフレームワークにとどまりません。同じパターンはどのプログラミング言語にも適用できます。モック マネージャーの構造は、TypeScript や JavaScript などでは大きく異なりますが、単体テストはほぼ同じになります。
これがお役に立てば幸いです!
以上が単体テストの MockManager - モックに使用されるビルダー パターンの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。