ユニットテストでは、コードの重複が許容されますか?


112

私はいくつかのユニットが、私が通過した時に前にいくつかの時間をテストし台無しにし、それらをより作るためにそれらをリファクタリングDRYない各テストの--the意図がもはや明らかでした。テストの可読性と保守性の間にはトレードオフがあるようです。ユニットテストで重複したコードを残しておくと読みやすくなりますが、SUTを変更した場合は、重複したコードの各コピーを追跡して変更する必要があります。

このトレードオフが存在することに同意しますか?もしそうなら、あなたはあなたのテストを読みやすくするか、維持することを好みますか?

回答:


68

重複したコードは、他のコードと同様に単体テストコードのにおいです。テストでコードを複製した場合は、更新するテストの数が極端に増えるため、実装コードのリファクタリングが難しくなります。テストは、テスト対象のコードでの作業を妨げる大きな負担になるのではなく、自信を持ってリファクタリングするのに役立ちます。

複製がフィクスチャのセットアップにある場合は、setUpメソッドをさらに活用するか、より多くの(またはより柔軟な)作成メソッドを提供することを検討してください。

SUTを操作するコードに重複がある場合は、いわゆる「単体」テストがまったく同じ機能を実行している理由を自問してください。

重複がアサーションにある場合、おそらくいくつかのカスタムアサーションが必要です。たとえば、複数のテストに次のような一連のアサーションがある場合:

assertEqual('Joe', person.getFirstName())
assertEqual('Bloggs', person.getLastName())
assertEqual(23, person.getAge())

次に、おそらく単一のassertPersonEqualメソッドが必要になるため、を記述できますassertPersonEqual(Person('Joe', 'Bloggs', 23), person)。(または、単に等価演算子をにオーバーロードする必要があるだけかもしれませんPerson。)

あなたが言うように、テストコードが読みやすいことが重要です。特に、テストの意図が明確であることが重要です。多くのテストがほとんど同じように見える場合(たとえば、4分の3の線が同じまたは実質的に同じ)、注意深く読んで比較しなければ、大きな違いを見つけて認識することは困難です。したがって、重複を削除するためのリファクタリング読みやすさに役立ちます。すべてのテストメソッドのすべての行がテストの目的に直接関連しているためです。これは、直接関連する行と単なる定型文である行のランダムな組み合わせよりも、読者にとってはるかに役立ちます。

とはいえ、テストでは、類似しているものの大幅に異なる複雑な状況を実行している場合があり、重複を減らすための適切な方法を見つけるのは困難です。常識を使用してください。テストが読みやすく、意図が明確で、テストによって呼び出されたコードをリファクタリングするときに、理論的に最小限の数以上のテストを更新する必要がある場合は、問題を受け入れて移動します。より生産的なものに移ります。いつでも戻って、後からテストをリファクタリングして、インスピレーションが得られます。


29
「重複したコードは、他のコードと同様に単体テストコードの匂いです。」いいえ。「テスト内のコードが重複していると、更新するテストの数が極端に増えるため、実装コードのリファクタリングが難しくなります。」これは、パブリックAPIではなくプライベートAPIをテストしているために発生します。

15
ただし、単体テストでコードが重複しないようにするには、通常、新しいロジックを導入する必要があります。単体テストにはロジックが含まれるべきではないと思います。単体テストの単体テストが必要になるからです。
Petr Peller、2015

@ user11617「プライベートAPI」と「パブリックAPI」を定義してください。私の理解では、パブリックApiは、外部の世界/サードパーティのコンシューマーに表示され、SemVerまたは同様のものを介して明示的にバージョン管理されるAPIであり、それ以外のものはプライベートです。この定義では、ほとんどすべてのユニットテストが「プライベートAPI」をテストしているため、コードの重複に敏感です。
KolA

@KolA "パブリック"はサードパーティの消費者を意味するものではありません-これはWeb APIではありません。クラスのパブリックAPIは、クライアントコードで使用されることを意図したメソッド(通常、それほど変更されない、または変更されるべきではない)を指します。通常は「パブリック」メソッドです。プライベートAPIは、内部で使用されるロジックとメソッドを指します。これらはクラスの外部からアクセスすべきではありません。これが、アクセス修飾子または使用する言語の規則を使用して、クラス内のロジックを正しくカプセル化することが重要である理由の1つです。
Nathan

@Nathan any library / dll / nugetパッケージにはサードパーティのコンシューマーがあり、Web APIである必要はありません。私が言及したのは、単体テストがそれらに直接到達できるようにするためだけに、ライブラリのコンシューマーによって直接使用されることを想定していないパブリッククラスとメンバーを宣言する(または、せいぜいそれらを内部にして、InternalsVisibleToAttributeでアセンブリに注釈を付ける)ことは非常に一般的です。実装と相まって
大量

185

読みやすさはテストにとってより重要です。テストが失敗した場合は、問題を明確にする必要があります。何が失敗したのかを正確に判断するために、開発者は多くの厳密に因数分解されたテストコードをたどる必要はありません。ユニットテストテストを作成する必要があるほどテストコードが複雑になりたくない。

ただし、何も不明瞭にならない限り、通常は重複を排除することをお勧めします。テストで重複を排除すると、APIが向上する可能性があります。収益が減少するポイントを超えないようにしてください。


xUnitなどのアサートコールには「メッセージ」引数が含まれています。開発者が失敗したテスト結果をすばやく見つけることができるように、意味のあるフレーズを配置することをお勧めします。
seand

1
@seandアサートがチェックしているものを説明しようとすることができますが、それが失敗し、ややあいまいなコードが含まれている場合、開発者はとにかくそれを巻き戻す必要があります。IMOそこに自己記述的なコードを含めることがより重要です。
IgorK 2012年

1
@クリストファー、?なぜこれがコミュニティウィキなのですか?
Pacerier、2015

@Pacerierわからない。以前は自動的にコミュニティウィキになることについて複雑なルールがありました。
クリストファージョンソン

レポートの読みやすさはテストよりも重要です。特に、統合またはエンドツーエンドのテストを行う場合、シナリオは非常に複雑になる可能性があるため、少しだけナビゲートすることは避けられます。問題を十分に説明してください。
Anirudh

47

実装コードとテストは異なる動物であり、ファクタリングルールはそれらに異なる方法で適用されます。

重複したコードまたは構造は、常に実装コードのにおいです。実装にボイラープレートを導入し始めたら、抽象化を修正する必要があります。

一方、テストコードは重複のレベルを維持する必要があります。テストコードの複製により、次の2つの目標が達成されます。

  • テストを切り離しておく。過剰なテストカップリングは、コントラクトが変更されたために更新が必要な単一の失敗したテストを変更することを困難にする可能性があります。
  • テストを分離して意味のあるものに保つ。単一のテストが失敗した場合、それが何をテストしているのかを正確に見つけることは、かなり簡単でなければなりません。

各テストメソッドが約20行未満である限り、私はテストコードのささいな重複を無視する傾向があります。セットアップ、実行、検証のリズムがテストメソッドで明らかになるのが好きです。

テストの「検証」部分で複製が忍び寄る場合、カスタムアサーションメソッドを定義すると効果的です。もちろん、これらのメソッドは、メソッド名で明らかにできる明確に識別された関係をテストする必要があります:assertPegFitsInHole->良い、assertPegIsGood->悪い。

テスト方法が長くて繰り返しの多いものになると、いくつかのパラメーターを取る空欄埋めテストテンプレートを定義すると便利なことがあります。次に、実際のテストメソッドは、適切なパラメーターを使用したテンプレートメソッドの呼び出しに削減されます。

プログラミングとテストに関する多くのことに関しては、明確な答えはありません。あなたは味を発達させる必要があり、そうするための最良の方法は間違いを犯すことです。


8

テストユーティリティメソッドのいくつかの異なるフレーバーを使用して、繰り返しを減らすことができます

私は本番コードよりもテストコードの繰り返しに寛容ですが、時々それに不満を感じています。クラスのデザインを変更し、戻ってすべてが同じセットアップ手順を実行する10の異なるテストメソッドを微調整する必要がある場合、イライラします。


7

同意する。トレードオフは存在しますが、場所によって異なります。

状態を設定するために、重複したコードをリファクタリングする可能性が高くなります。しかし、実際にコードを実行するテストの部分をリファクタリングする可能性は低くなります。とは言っても、コードの実行に常に数行のコードが必要な場合は、それをにおいだと考えて、テスト中の実際のコードをリファクタリングするかもしれません。これにより、コードとテストの両方の可読性と保守性が向上します。


これは良い考えだと思います。重複が多い場合は、リファクタリングして、多くのテストを実行できる共通の「テストフィクスチャ」を作成できるかどうかを確認してください。これにより、セットアップ/ティアダウンコードの重複がなくなります。
アウトロープログラマ

6

Jay Fieldsは、「DSLはDRYではなくDAMPである必要がある」というフレーズを作り出しました。DAMP説明的で意味のあるフレーズを意味します。同じことがテストにも当てはまると思います。明らかに、あまりにも多くの複製は悪いです。しかし、すべてのコストで重複を取り除くことはさらに悪いことです。テストは、意図を明らかにする仕様として機能する必要があります。たとえば、同じフィーチャを複数の異なる角度から指定した場合、ある程度の重複が予想されます。


3

このため、rspecが大好きです。

役立つ2つのこと-

  • 一般的な動作をテストするためのサンプルグループを共有しました。
    テストのセットを定義し、そのセットを実際のテストに「含める」ことができます。

  • ネストされたコンテキスト。
    基本的に、クラスのすべてのテストだけでなく、テストの特定のサブセットに対して「セットアップ」および「ティアダウン」メソッドを使用できます。

.NET / Java /その他のテストフレームワークがこれらのメソッドを採用するのが早ければ早いほどよい(または、IronRubyまたはJRubyを使用してテストを作成することができます。


3

テストコードには、通常は量産コードに適用されるのと同じレベルのエンジニアリングが必要だと思います。読みやすさを優先して行われた議論は確かにあり得、私はそれが重要であることに同意します。

しかし、私の経験では、よく因数分解されたテストの方が読みやすく、理解しやすいことがわかりました。変更された1つの変数と最後のアサーションを除いて、それぞれが同じように見える5つのテストがある場合、その単一の異なる項目が何であるかを見つけるのは非常に困難です。同様に、変化する変数とアサーションのみが表示されるように因数分解されている場合、テストがすぐに何を行っているかを理解するのは簡単です。

テストが困難な場合に適切な抽象化レベルを見つけることは難しいので、実行する価値はあると思います。


2

重複したコードと読みやすいコードの間に関係があるとは思いません。あなたのテストコードは他のコードと同じくらい良いはずです。繰り返しを行わないコードは、適切に実行すると、重複したコードよりも読みやすくなります。


2

理想的には、いったん作成された単体テストはそれほど変更されないので、読みやすさを重視します。

単体テストをできる限り個別にすることも、対象とする特定の機能にテストを集中させるのに役立ちます。

そうは言っても、私は何度も繰り返し使用する特定のコード(たとえば、一連のテスト全体でまったく同じセットアップコード)を試して再利用する傾向があります。


2

「より乾燥するようにそれらをリファクタリングしました-各テストの意図はもはや明確ではありませんでした」

リファクタリングの実行に問題があったようです。私は推測しているだけですが、それが明らかに不明確になったとしても、完全に明確である適度にエレガントなテストを行うために、まだやらなければならないことがまだないのですか?

テストがUnitTestのサブクラスであるのはそのためです。そのため、正確で、検証とクリアが容易な優れたテストスイートを設計できます。

昔は、さまざまなプログラミング言語を使用するテストツールがありました。テストを使用して快適で作業しやすいように設計することは困難(または不可能)でした。

あなたは-Python、Java、C#-を使っているどんな言語の能力も持っているので、その言語を上手に使ってください。明確で冗長すぎない見栄えの良いテストコードを実現できます。トレードオフはありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.