.NET単体テスト:前払いして後で節約する

公開: 2022-03-11

ユニットテストについて利害関係者やクライアントと話し合うとき、ユニットテストに関して多くの混乱と疑問が生じることがよくあります。 ユニットテストは、デンタルフロスが子供に与えるように聞こえることがあります。 「私はすでに歯を磨いています。なぜこれを行う必要があるのですか?」

単体テストを提案することは、テスト方法とユーザー受け入れテストが十分に強力であると考える人々にとって、不必要な費用のように聞こえることがよくあります。

しかし、ユニットテストは非常に強力なツールであり、想像以上に簡単です。 この記事では、単体テストと、 Microsoft.VisualStudio.TestToolsMoqなどのDotNetで使用できるツールについて説明します。

フィボナッチ数列のn番目の項を計算する単純なクラスライブラリの構築を試みます。 そのためには、数値を加算するカスタム数学クラスに依存するフィボナッチ数列を計算するためのクラスを作成する必要があります。 次に、.NET Testing Frameworkを使用して、プログラムが期待どおりに実行されることを確認できます。

ユニットテストとは何ですか?

単体テストは、プログラムをコードの最小ビット(通常は関数レベル)に分解し、関数が期待する値を返すことを確認します。 単体テストフレームワークを使用することにより、単体テストは独立したエンティティになり、プログラムの構築中にプログラムで自動テストを実行できます。

 [TestClass] public class FibonacciTests { [TestMethod] //Check the first value we calculate public void Fibonacci_GetNthTerm_Input2_AssertResult1() { //Arrange int n = 2; //setup Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>(); mockMath .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>())) .Returns((int x, int y) => x + y); UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object); //Act int result = fibonacci.GetNthTerm(n); //Assert Assert.AreEqual(result, 1); } }

Arrange、Act、Assertの方法論テストを使用した単純な単体テストで、数学ライブラリが2+2を正しく追加できることをテストします。

単体テストが設定された後、コードに変更が加えられた場合、たとえば、プログラムが最初に開発されたときに不明であった追加の条件を考慮して、単体テストはすべてのケースが期待値と一致するかどうかを示します関数によって出力されます。

単体テストは統合テストではありません。 エンドツーエンドのテストではありません。 これらは両方とも強力な方法論ですが、代替としてではなく、単体テストと組み合わせて機能する必要があります。

ユニットテストの利点と目的

ユニットテストの理解するのが最も難しい利点ですが、最も重要なのは、変更されたコードをその場で再テストできることです。 理解しにくいのは、多くの開発者が「二度とその機能に触れない」 「終わったら再テストする」と考えているからです。 そして、利害関係者は、 「その部分がすでに書かれているのに、なぜそれを再テストする必要があるのか​​」という観点から考えます。

開発スペクトルの両側にいる人として、私はこれらの両方のことを言いました。 私の中の開発者は、なぜそれを再テストしなければならないのかを知っています。

私たちが日常的に行う変更は、大きな影響を与える可能性があります。 例えば:

  • スイッチは、入力した新しい値を適切に考慮していますか?
  • そのスイッチを何回使用したか知っていますか?
  • 大文字と小文字を区別しない文字列比較を適切に考慮しましたか?
  • nullを適切にチェックしていますか?
  • スロー例外は期待どおりに処理されますか?

ユニットテストでは、これらの質問を受け取り、コードとプロセスでそれらを記憶して、これらの質問に常に回答できるようにします。 ビルドの前に単体テストを実行して、新しいバグが発生していないことを確認できます。 単体テストはアトミックに設計されているため、非常に高速に実行され、通常はテストごとに10ミリ秒未満です。 非常に大規模なアプリケーションでも、完全なテストスイートを1時間以内に実行できます。 UATプロセスはそれに一致しますか?

テストするクラスまたはクラス内のメソッドを簡単に検索するために設定された命名規則の例。
最初の実行でセットアップ時間を含むFibonacci_GetNthTerm_Input2_AssertResult1を除いて、すべての単体テストは5ミリ秒未満で実行されます。 ここでの私の命名規則は、テストしたいクラスまたはクラス内のメソッドを簡単に検索するように設定されています

しかし、開発者としては、これはあなたにとってより多くの作業のように聞こえるかもしれません。 はい、リリースするコードが優れているという安心感が得られます。 しかし、単体テストは、設計がどこに弱いのかを確認する機会も提供します。 2つのコードに対して同じ単体テストを作成していますか? 代わりに、それらを1つのコードに含める必要がありますか?

コード自体をユニットテスト可能にすることは、設計を改善するための方法です。 また、単体テストを行ったことがない、またはコーディング前に設計を検討するのにそれほど時間がかからないほとんどの開発者にとって、単体テストの準備をすることで、設計がどれだけ改善されるかを理解できます。

コードユニットはテスト可能ですか?

DRYの他に、他の考慮事項もあります。

あなたのメソッドや関数はやりすぎですか?

予想よりも長く実行される非常に複雑な単体テストを作成する必要がある場合は、メソッドが複雑すぎて、複数のメソッドとして適している可能性があります。

依存性注入を適切に活用していますか?

テスト対象のメソッドが別のクラスまたは関数を必要とする場合、これを依存関係と呼びます。 単体テストでは、依存関係が内部で何をしているのかは気にしません。 テスト対象のメソッドの目的上、これはブラックボックスです。 依存関係には、その動作が適切に機能しているかどうかを判断する独自の単体テストのセットがあります。

テスターとして、その依存関係をシミュレートし、特定のインスタンスで返す値を指示する必要があります。 これにより、テストケースをより細かく制御できます。 これを行うには、その依存関係のダミー(または後で説明するようにモック)バージョンを挿入する必要があります。

コンポーネントは互いにどのように相互作用しますか?

依存性と依存性注入を理解すると、コードに循環依存性が導入されていることに気付くかもしれません。 クラスAがクラスBに依存し、クラスBがクラスAに依存している場合は、設計を再検討する必要があります。

依存性注入の美しさ

フィボナッチの例を考えてみましょう。 上司は、C#で使用可能な現在の追加演算子よりも効率的で正確な新しいクラスがあると言っています。

この特定の例は現実の世界ではあまりありそうにありませんが、認証、オブジェクトマッピング、およびほとんどすべてのアルゴリズムプロセスなど、他のコンポーネントでも同様の例が見られます。 この記事の目的のために、クライアントの新しい追加機能が、コンピューターが発明されて以来、最新かつ最高のものであると仮定しましょう。

そのため、上司から、単一のクラスMathを含むブラックボックスライブラリが渡され、そのクラスでは、単一の関数Addが渡されます。 フィボナッチ計算機を実装するあなたの仕事は、次のようになります。

 public int GetNthTerm(int n) { Math math = new Math(); int nMinusTwoTerm = 1; int nMinusOneTerm = 1; int newTerm = 0; for (int i = 2; i < n; i++) { newTerm = math.Add(nMinusOneTerm, nMinusTwoTerm); nMinusTwoTerm = nMinusOneTerm; nMinusOneTerm = newTerm; } return newTerm; }

これは恐ろしいことではありません。 新しいMathクラスをインスタンス化し、それを使用して前の2つの用語を追加し、次の用語を取得します。 この方法は、通常の一連のテストを実行し、100項まで計算し、1000項、10,000項などを計算して、方法論が正常に機能することに満足するまで続けます。 その後、将来のある時点で、ユーザーは第501軍団が期待どおりに機能していないと不満を漏らします。 あなたは夜を過ごしてコードを調べ、このコーナーケースが機能しない理由を理解しようとします。 あなたは、最新で最高のMathのクラスが上司が思っているほど素晴らしいものではないことに疑いを持ち始めます。 しかし、それはブラックボックスであり、それを実際に証明することはできません。内部的に行き詰まりに陥っています。

ここでの問題は、依存関係のMathがフィボナッチ計算機に注入されていないことです。 したがって、テストでは、フィボナッチをテストするために、 Mathからの既存の、テストされていない、未知の結果に常に依存します。 Mathに問題がある場合、フィボナッチは常に間違っています(501番目の用語の特別なケースをコーディングしないと)。

この問題を修正するためのアイデアは、 Mathクラスをフィボナッチ計算機に注入することです。 しかし、さらに良いのは、パブリックメソッド(この場合はAdd )を定義するMathクラスのインターフェイスを作成し、 Mathクラスにインターフェイスを実装することです。

 public interface IMath { int Add(int x, int y); } public class Math : IMath { public int Add(int x, int y) { //super secret implementation here } } }

Mathクラスをフィボナッチに注入するのではなく、 IMathインターフェイスをフィボナッチに注入することができます。 ここでの利点は、正確であることがわかっている独自のOurMathクラスを定義し、それに対して計算機をテストできることです。 さらに良いことに、Moqを使用すると、 Math.Addが返すものを簡単に定義できます。 合計の数を定義することも、 Math.Addにx+yを返すように指示することもできます。

 private IMath _math; public Fibonacci(IMath math) { _math = math; }

IMathインターフェースをフィボナッチクラスに注入します

 //setup Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>(); mockMath .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>())) .Returns((int x, int y) => x + y);

Moqを使用して、 Math.Addが返すものを定義します。

これで、2つの数値を加算するための実証済みの(C#で+演算子が間違っている場合は、より大きな問題が発生します)メソッドができました。 新しいIMathを使用して、501番目の用語の単体テストをコーディングし、実装を間違えたかどうか、またはカスタムMathクラスにもう少し作業が必要かどうかを確認できます。

メソッドにやりすぎをさせないでください

この例は、メソッドがやりすぎであるという考えも示しています。 確かに、加算はかなり単純な操作であり、 GetNthTermメソッドからその機能を抽象化する必要はほとんどありません。 しかし、操作がもう少し複雑な場合はどうでしょうか。 追加の代わりに、おそらくそれはモデルの検証、操作するオブジェクトを取得するためのファクトリの呼び出し、またはリポジトリから追加の必要なデータの収集でした。

ほとんどの開発者は、1つの方法には1つの目的があるという考えに固執しようとします。 単体テストでは、単体テストはアトミックメソッドに適用する必要があるという原則に固執するように努めており、メソッドに多くの操作を導入することで、テスト不能にします。 関数を適切にテストするために非常に多くのテストを作成しなければならないという問題が発生することがよくあります。

メソッドに追加する各パラメーターは、パラメーターの複雑さに応じて指数関数的に記述しなければならないテストの数を増やします。 ロジックにブール値を追加する場合は、現在のテストと一緒に真と偽のケースをチェックする必要があるため、書き込むテストの数を2倍にする必要があります。 モデル検証の場合、単体テストの複雑さは非常に急速に増大する可能性があります。

ブール値がロジックに追加されたときに必要となる増加したテストの図。

私たちは皆、メソッドに少し余分なものを追加することで罪を犯しています。 しかし、これらのより大きく、より複雑な方法では、単体テストが多すぎる必要があります。 そして、ユニットテストを書くと、メソッドがやりすぎであることがすぐに明らかになります。 入力パラメーターから考えられる結果をテストしすぎていると思われる場合は、メソッドを一連の小さなメソッドに分割する必要があるという事実を考慮してください。

繰り返さないでください

プログラミングの私たちのお気に入りのテナントの1つ。 これはかなり簡単なはずです。 同じテストを複数回作成していることに気付いた場合は、コードを複数回導入しています。 その作業を、使用しようとしている両方のインスタンスがアクセスできる共通のクラスにリファクタリングすると便利な場合があります。

どのユニットテストツールが利用できますか?

DotNetは、箱から出してすぐに使用できる非常に強力な単体テストプラットフォームを提供します。 これを使用して、アレンジ、アクト、アサートの方法論として知られているものを実装できます。 最初の考慮事項を調整し、テスト対象のメソッドを使用してそれらの条件に基づいて行動し、次に何かが起こったことを表明します。 あなたは何でも主張することができ、このツールをさらに強力にします。 メソッドが特定の回数呼び出されたこと、メソッドが特定の値を返したこと、特定のタイプの例外がスローされたこと、またはその他の考えられることを表明できます。 より高度なフレームワークを探している人にとって、NUnitとそれに対応するJavaのJUnitは実行可能なオプションです。

 [TestMethod] //Test To Verify Add Never Called on the First Term public void Fibonacci_GetNthTerm_Input0_AssertAddNeverCalled() { //Arrange int n = 0; //setup Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>(); mockMath .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>())) .Returns((int x, int y) => x + y); UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object); //Act int result = fibonacci.GetNthTerm(n); //Assert mockMath.Verify(r => r.Add(It.IsAny<int>(), It.IsAny<int>()), Times.Never); }

例外をスローすることにより、フィボナッチメソッドが負の数を処理することをテストします。 単体テストでは、例外がスローされたことを確認できます。

依存性注入を処理するために、NinjectとUnityの両方がDotNetプラットフォームに存在します。 この2つにはほとんど違いがなく、FluentSyntaxまたはXMLConfigurationを使用して構成を管理するかどうかが問題になります。

依存関係をシミュレートするには、Moqをお勧めします。 Moqは手に入れるのが難しい場合がありますが、要点は、依存関係のモックバージョンを作成することです。 次に、特定の条件下で何を返すかを依存関係に指示します。 たとえば、整数を2乗するSquare(int x)という名前のメソッドがある場合、x = 2のときに4を返すように指示できます。また、任意の整数に対してx^2を返すように指示することもできます。 または、x = 2の場合に5を返すように指示することもできます。最後のケースを実行するのはなぜですか? テストの役割の下にあるメソッドが依存関係からの回答を検証することである場合、バグを適切にキャッチしていることを確認するために、無効な回答を強制的に返すことができます。

 [TestMethod] //Test To Verify Add Called Three times on the fifth Term public void Fibonacci_GetNthTerm_Input4_AssertAddCalledThreeTimes() { //Arrange int n = 4; //setup Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>(); mockMath .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>())) .Returns((int x, int y) => x + y); UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object); //Act int result = fibonacci.GetNthTerm(n); //Assert mockMath.Verify(r => r.Add(It.IsAny<int>(), It.IsAny<int>()), Times.Exactly(3)); }

Moqを使用して、モックされたIMathインターフェイスにテスト対象のAddの処理方法を指示します。 It.Isを使用して明示的なケースを設定するか、 It.Isを使用して範囲を設定It.IsInRangeます。

DotNetのユニットテストフレームワーク

Microsoftユニットテストフレームワーク

Microsoftユニットテストフレームワークは、Microsoftが提供する、すぐに使用できるユニットテストソリューションであり、VisualStudioに含まれています。 VSが付属しているので、うまく統合できます。 プロジェクトを開始すると、Visual Studioは、アプリケーションの横に単体テストライブラリを作成するかどうかを尋ねてきます。

Microsoftユニットテストフレームワークには、テスト手順をより適切に分析するのに役立つツールも多数付属しています。 また、マイクロソフトが所有・作成しているため、今後の存在感はある程度安定しています。

しかし、Microsoftツールを使用すると、それらが提供するものを手に入れることができます。 Microsoftユニットテストフレームワークは、統合するのが面倒な場合があります。

NUnit

NUnitを使用する上での最大の利点は、パラメーター化されたテストです。 上記のフィボナッチの例では、いくつかのテストケースを入力して、それらの結果が正しいことを確認できます。 また、501番目の問題の場合は、いつでも新しいパラメーターセットを追加して、新しいテストメソッドを必要とせずにテストが常に実行されるようにすることができます。

NUnitの主な欠点は、VisualStudioに統合することです。 Microsoftバージョンに付属しているベルやホイッスルがなく、独自のツールセットをダウンロードする必要があります。

xUnit.Net

xUnitは、既存の.NETエコシステムと非常にうまく統合されるため、C#で非常に人気があります。 Nugetには、xUnitの多くの拡張機能があります。 また、Team Foundation Serverとうまく統合されていますが、さまざまなGit実装でTFSを使用している.NET開発者の数はわかりません。

欠点として、多くのユーザーがxUnitのドキュメントが少し不足していると不満を漏らしています。 単体テストの新規ユーザーにとって、これは大きな頭痛の種となる可能性があります。 さらに、xUnitの拡張性と適応性により、学習曲線はNUnitまたはMicrosoftのユニットテストフレームワークよりも少し急になります。

テスト駆動開発/開発

テスト駆動開発(TDD)は、独自の投稿に値するもう少し高度なトピックです。 しかし、私は紹介を提供したかった。

アイデアは、ユニットテストから始めて、ユニットテストに何が正しいかを伝えることです。 次に、それらのテストを中心にコードを記述できます。 理論的には、概念は単純に聞こえますが、実際には、アプリケーションについて後ろ向きに考えるように脳を訓練することは非常に困難です。 ただし、このアプローチには、事後に単体テストを作成する必要がないという組み込みの利点があります。 これにより、リファクタリング、書き換え、およびクラスの混乱が少なくなります。

TDDは近年少し流行語になっていますが、採用は遅れています。 その概念的な性質は、利害関係者を混乱させ、承認を得るのを困難にします。 しかし、開発者として、プロセスに慣れるためにTDDアプローチを使用して小さなアプリケーションを作成することをお勧めします。

ユニットテストが多すぎない理由

単体テストは、開発者が自由に使える最も強力なテストツールの1つです。 アプリケーションの完全なテストには決して十分ではありませんが、回帰テスト、コード設計、および目的の文書化におけるその利点は比類のないものです。

ユニットテストを書きすぎるようなことはありません。 各エッジケースは、ソフトウェアの将来的に大きな問題を提案する可能性があります。 発見されたバグを単体テストとして記憶することで、それらのバグが後のコード変更中にソフトウェアに忍び寄る方法を見つけられないようにすることができます。 プロジェクトの初期予算に10〜20%を追加することもできますが、トレーニング、バグ修正、およびドキュメント化でそれよりもはるかに節約できます。

この記事で使用されているBitbucketリポジトリはここにあります。