web-dev-qa-db-ja.com

TDDでは、単体テスト用のヘルパークラスを記述することは悪い習慣ですか?

テスト駆動開発(TDD)を使用して単体テストを作成するときに、結果が正しいかどうかの確認が「1行のコードよりも複雑」であり、おそらく独自の機能に値する場合があります。

その場合、補助的なメソッドやヘルパー/拡張クラスを書くことは頭に浮かぶかもしれません。たとえば、多くのテストケースで同じ操作を実行している場合は、リファクタリングしてそのテストの可読性を向上させてみませんか?

しかし、問題が発生します。テストケースの作成方法を改善するためにユーティリティ/ヘルパークラスを作成している場合、それを単体テストする必要がありますか?ヘルパークラスをまったく作成する必要がありますか?それは悪い習慣ですか? ?


例証するために、私は削減された例を挙げます:

次のパブリックAPI要件があるとします。

  • MyListに値を追加できるようにする必要があります
  • これらの値を取得する必要があります。 順序は関係ありません

それを満足させるために、私は次のテストを書いています:

public class MyListTests
{
    [Test]
    public void Add_AddingValues_ValuesArePresentInList()
    {
        MyList myList = new MyList();
        myList.Add(3);
        myList.Add(1);
        myList.Add(1);
        myList.Add(2);

        int[] expectedValues = new int[] { 1, 1, 2, 3 };
        // Assert that myList.GetValues have the same elements as expectedValues;  
    }
}

しかしながら、 // Assert that myList.GetValues have the same elements as expectedValues;は、順序が重要でない場合は、それ自体では非常に重要なタスクです。実際、2つの列挙可能な要素が同じ要素セット(出現数を含む)であるかどうかをテストすることは、独自の機能であると主張することができます。

この状況をどのように処理する必要がありますか?このテストケースをより適切に許可するためにヘルパークラスを実装する必要がありますか?このヘルパークラスをユニットテストする必要がありますか?


補足として、両方の実装がテストを満たす必要があります。

実装A

public class MyList
{
    private List<int> values = new List<int>;

    public void Add(int value)
    {
        values.Add(value);
    }

    public void IReadOnlyCollection<int> GetValues()
    {
        return values;
    }
}

実装B

public class MyList
{
    private List<int> values = new List<int>;

    public void Add(int value)
    {
        List<int> newValueAsList = new List<int>();
        newValueAsList.Add(value);

        this.values = newValueAsList.Concat(this.values);
    }

    public void IReadOnlyCollection<int> GetValues()
    {
        return values;
    }
}

これは、TDDを使用した動作のテストに関する質問です。実装ではありません。 MyListの実装の効率は問題ではありません。

1
Albuquerque

あなたの例はおそらく、ヘルパークラスがなぜ有用であるのか、そしてヘルパークラスが独自にテストを必要とするのかもしれない理由を説明するには、あまりにも不自然です。しかし、私はここで、すべてNUnitによって駆動される多くの自動テストで作業しており、いくつかのヘルパークラスがあります。

  • 特定のデータ形式の出力を脆弱でない方法で比較する(独自の形式、または特定のCSVまたはXML形式、標準CADファイル形式など))

  • 特定の種類のテストデータを提供する(特に、データ自体をシリアル化された形式で格納するよりも、プログラムでデータを生成する方が簡単で保守しやすい場合)

  • より複雑なテスト手順の一部を制御するため

正直に言うと、これらのヘルパークラスのほとんどは、実際には単体テストではない自動テストでより大きな役割を果たします。しかし、それらは私たちにとって非常に有用ですが、それらのいくつかはおそらく単体で単体テストに値するでしょう(私たちは現在、これらのクラスの多くの単体テストがないと思います)。そのようなクラスを賢明なテストプロセスに使用または導入することは、まったく逆です。

5
Doc Brown

これは本質的にクリーンなテストを書くことについての質問です

質問に答えるために、私は参考として本 Clean Code:A Handbook of Agile Software Craftsmanship を使用します。

本の第9章ユニットテストについて説明し、ほとんどのアドレスを示しますすべてではないにしても)尋ねられた質問のすべて。

ユーティリティクラスはドメイン固有のテスト言語です

これはそのように定義されています:

(...)プログラマーがシステムの操作に使用するAPIを使用するのではなく、これらのAPIを使用し、テストをより便利にする一連の関数とユーティリティを構築します書きやすく読みやすい。これらの関数とユーティリティは、テストで使用される特殊なAPIになります。これらは、プログラマーがテストの作成を支援し、後でそれらのテストを読む必要がある人を支援するために使用するテスト言語です。

これがまさに求められていることです。著者は、ドメイン固有のテスト言語を書くことはオプションであるだけでなく、テスト品質を向上させるためのステップでもあると考えています。

なぜこれらのユーティリティクラスの作成に注意する必要があるのですか?

(...)テストは、製品コードが進化するにつれて変更する必要があります。より汚れたテストは、変更が難しくなります。テストコードが絡み合うほど、新しいテストコードをスイートに詰め込むのに、新しい本番コードを記述するよりも多くの時間を費やす可能性が高くなります。本番コードを変更すると、古いテストが失敗し始め、テストコードの混乱により、それらのテストを再度パスすることが難しくなります。したがって、テストはますます増大する責任と見なされるようになります。

著者は明確に述べています:

ストーリーのモラルは単純です:テストコードは製品コードと同様に重要です。二流の市民ではありません。思考、デザイン、ケアが必要です。本番コードと同じくらいクリーンに保つ必要があります。

これらの引用から、質問In TDD, is it bad practice to write helper class for unit tests?は、本番環境で作成されるコードと同じように注意して処理する必要があります。

クリーンテストとは何ですか?

三つの事。読みやすさ、読みやすさ、読みやすさ。可読性は、おそらく本番用コードよりも単体テストでさらに重要です。テストを読みやすくするものは何ですか?すべてのコードを読みやすくする同じもの:明快さ、単純さ、表現の密度。テストでは、できるだけ少ない表現で多くのことを言いたいと思います。

これで、テストに可読性を向上させることができるコードがある場合、それはそうである必要があることは明らかです。これにより、テストが読みやすくなり、テストの品質が向上します。同じ10行のコードを記述して2つのリストを特定の方法で比較する場合は、テストの読みやすさを向上させるために、これらの10行を別の方法でリファクタリングすることをお勧めします。

ユーティリティクラスは読みやすさを向上させます

これらのユーティリティクラスの作成に注意が必要なのはいつですか。

このテストAPIは事前に設計されていません;むしろ、詳細を難読化することによって汚染されすぎたテストコードの継続的なリファクタリングから進化しています。

単体テストでチェックする必要があるパブリックAPIの一部として設計することはありません。このテストAPIは、テストが大きくなるにつれて、またテストのリファクタリングの一部として設計されます。

@Doc Brownが述べたように:

TDDサイクル( "red-green-refactor")は、コンポーネントを事前に設計するのではなく、継続的なリファクタリングによってコンポーネントを進化させることです。

これは、ユーティリティクラスの記述がTDDサイクルの3番目のステップに該当することを意味します。リファクタリングのステップ。

ユーティリティクラスはどこに置くべきですか?

Xユニットパターンテストユーティリティメソッドの概念を定義し、実装の詳細ガイドラインに次のテキストが含まれています。

再利用可能なテストユーティリティメソッドの記述は非常に簡単です。より大きな問題は、どこに置くかです。テストユーティリティメソッドが単一のテストケースクラス(ページX)のテストメソッドでのみ必要な場合、そのクラスにテストユーティリティメソッドを配置できますが、複数のクラスからそれを必要とする場合、ソリューションは少し複雑になります。それはすべて型の可視性に帰着します。クライアントクラスは、テストユーティリティメソッドを表示できる必要があり、テストユーティリティメソッドは、依存するすべてのタイプとクラスを表示できる必要があります。多くに依存しない場合、または依存するすべてが1つの場所から見える場合、テストユーティリティメソッドは、プロジェクトまたは会社に対して定義する共通のテストケーススーパークラス(Xページ)に配置できます。すべてのクライアントが見ることができる単一の場所からは見えないタイプ/クラスに依存する場合、適切なテストパッケージまたはサブシステムのテストヘルパーに配置する必要がある場合があります。ドメインオブジェクトのグループが多数ある大規模なシステムでは、関連するドメインオブジェクトのグループ(パッケージ)ごとに1つのテストヘルパーがあるのが一般的です。


1つのユニットでユーティリティクラスをテストする必要がありますか?

この質問はすでにさまざまな形で行われています。

ユニットテストユーティリティクラス

最もよく投票された答えは明確に述べています:

(...)はい、ユーティリティメソッドのテストを記述します。いいえ、他のテストから切り離そうとしないでください。単純なユーティリティ関数が、独自のテストで検証されたとおりに正しく機能すると仮定します。これ以上何をしても、何の利益も得られません。

これは基本的に、ユーティリティメソッドによって提供される機能は、ターゲットのパブリックAPI用に作成されているユニットテストと組み合わせてテストされることを意味しています。テストで使用しているユーティリティメソッドにバグがある場合、それはテストしているパブリックAPIの失敗したテストとして表示されます。

これは、質問をするときにも見ることができます:ユニットテストをユニットテストする必要がありますか?もちろんそうではありません。 TDDの全体的なアイデアは、以下の前提でターゲットコードの望ましい動作を検証するための単純な単体テストを作成することです。

  • 単体テストコードは、簡単に記述して検証できるように十分にシンプルである必要があります
  • テストケースにバグがある場合、それはビジネスロジックに対する失敗したテストとして表示されます。

同じ推論を使用することにより、ユーティリティメソッドは検証のために十分に単純で信頼できるはずです。そこにバグがある場合は、とにかく書かれているテストに表示されるはずです。

テストユーティリティメソッドは、作成しているテストを単純化したものであり、そのように処理する必要があります。

さらに、次の質問の議論を検討してください。

TDDでは、リファクタリングされたコードに単体テストを追加する必要がありますか?

多くの答えで述べたように:

プログラマーテストは、動作の変化に敏感で、構造の変化に鈍感でなければなりません。 - ケントベック、2019

単体テストで重複したコードのリファクタリングとして定義されているユーティリティメソッドは、定義により動作の変化に影響されません。したがって、テストしないでください。

リファクタリングは、既存のコード本体を再構築し、外部の動作を変更せずに内部構造を変更するための統制のとれた手法です。

また:

単体テストは動作をテストするために特別に記述されているため、リファクタリング後に追加の単体テストを要求することは意味がありません。

リファクタリングによって、以前は必要でなかった追加の単体テストが必要になることはありません。


まとめ

  • TDDでは、単体テスト用のヘルパークラスを記述することは悪い習慣ですか?
    番号!どういたしまして!実際、テストコードの本来の品質である可読性が向上するため、これは良い習慣です。 1つの場所でのみ使用される多くのヘルパークラスの記述を開始する前に、重複するコードをヘルパークラスにリファクタリングする前に、まずユニットテストを機能させることをお勧めします。

  • テストヘルパークラスをユニットテストする必要がありますか?
    直接ではありません。ヘルパークラスは、単体テストを読みやすくするためのリファクタリングの結果としてのみ存在する必要があります。したがって、通常の単体テストと同様に、単体テストを行うべきではありません。それらの正しい動作は、ターゲットパブリックAPI用に記述されているユニットテストとの結合によって間接的にテストされます。何か問題がある場合、テストは失敗するはずです。

さらに、ホイールを再発明しないでください

単体テストは単純なはずです。そのため、多くの場合、使用されている言語またはテストフレームワークは、希望する重複したコードを達成するためのクリーンな方法を提供します。

そのため、既存のメソッドを使用して、独自のユーティリティクラスを実装することをお勧めします。ホイールを再発明しないでください。ユーティリティクラスではなく、実装とテストに関するこの質問全体は、多くの場合完全に回避できます。

たとえば、2つのコレクションの要素が同じであるかどうかを比較する例は、 CollectionAssert.AreEquivalent(ICollection、ICollection) などのメソッドで解決できます。 (質問のコメントでそれを指摘してくれた@Greg Burghardtに感謝します)。

ドメイン固有のテスト言語の概念が定義されたのは偶然ではありません。これらのユーティリティクラスは、一般的なデータ構造ではなく、アプリケーションの[〜#〜] domain [〜#〜]を処理する場合によく正当化されます。そのため、通常は標準的な実装があります。

2
Albuquerque

簡単に言えば、テストプロジェクトは他のプロジェクトとそれほど変わらないということです。あなたの場合、ヘルパーメソッドは特定のテストの外で生きるのに十分一般的であるようです。別のオプションは、特定のテストのヘルパーメソッドである場合は、テスト内にネストすることです。

余談ですが、複数の入力/出力セットでテストできるテストフレームワーク(nUnitなど)があることをお伝えしたいと思います。

0
Noceo