テストvs自分自身を繰り返さない(DRY)


11

テストを書くことで自分自身を繰り返すことがなぜ非常に奨励されるのですか

テストは基本的にコードと同じことを表しているため、コードの(概念ではなく、実装で)重複しているようです。DRYの最終的なターゲットには、すべてのテストコードの削除が含まれませんか?

回答:


24

これは私が考えることができるどんな方法でも誤解だと思います。

production-codeをテストするtest-codeはまったく似ていません。私はPythonでデモンストレーションします:

def multiply(a, b):
    """Multiply ``a`` by ``b``"""
    return a*b

次に、簡単なテストは次のようになります。

def test_multiply():
    assert multiply(4, 5) == 20

両方の関数の定義は似ていますが、どちらも非常に異なることを行います。ここに重複コードはありません。;-)

また、テスト機能ごとに1つのアサーションを持つ本質的に重複したテストを書くこともあります。これは狂気であり、私はこれをしている人々を見ました。これ悪い習慣です。

def test_multiply_1_and_3():
    """Assert that a multiplication of 1 and 3 is 3."""
    assert multiply(1, 3) == 3

def test_multiply_1_and_7():
    """Assert that a multiplication of 1 and 7 is 7."""
    assert multiply(1, 7) == 7

def test_multiply_3_and_4():
    """Assert that a multiplication of 3 and 4 is 12."""
    assert multiply(3, 4) == 12

1000行以上の有効なコード行に対してこれを行うことを想像してください。代わりに、「機能」ごとにテストします。

def test_multiply_positive():
    """Assert that positive numbers can be multiplied."""
    assert multiply(1, 3) == 3
    assert multiply(1, 7) == 7
    assert multiply(3, 4) == 12

def test_multiply_negative():
    """Assert that negative numbers can be multiplied."""
    assert multiply(1, -3) == -3
    assert multiply(-1, -7) == 7
    assert multiply(-3, 4) == -12

これで、機能が追加/削除されたときに、1つのテスト関数の追加/削除を検討するだけで済みます。

forループを適用していないことに気づいたかもしれません。これは、いくつかのことを繰り返すのが良いからです。ループを適用すると、コードはずっと短くなります。しかし、アサーションが失敗すると、あいまいなメッセージを表示する出力を難読化する可能性があります。これが発生すると、テストの有用性が低下し、どこで問題が発生したかを検査するデバッガ必要になります。


8
テストごとに1つのアサーションが技術的に推奨されます。これは、複数の問題が単一の失敗として表示されないことを意味するためです。ただし、実際には、アサーションを慎重に集約すると、繰り返されるコードの量が減り、テストガイドラインごとに1つのアサーションに固執することはほとんどないと思います。
ロブ教会14年

@ pink-diamond-squareアサーションが失敗した後、NUnitはテストを停止しません(これは奇妙だと思います)。その特定のケースでは、テストごとに1つのアサーションを持つ方が確かに優れています。ユニットテストフレームワークが、アサーションの失敗後にテストを停止する場合、複数のアサーションの方が優れています。
siebz0r 14年

3
NUnitはテストスイート全体を停止しませんが、それを防ぐための措置を講じない限り、その1つのテストは停止します(スローされる例外をキャッチできます。これはときどき役立ちます)。彼らが作っていると思う点は、複数のアサートを含むテストを書くと、問題を修正するために必要なすべての情報を取得できないということです。あなたの例を介して作業するために、この乗算関数は数値3を好まないと想像してください。この場合、assert multiply(1,3)失敗しますが、失敗したテストレポートも取得しませんassert multiply(3,4)
ロブ教会14年

テストごとに1つのアサートは、.netの世界で読んだことから、「グッドプラクティス」であり、複数のアサートは「実用的な使用法」であるため、私はそれを提起すると思いました。この例が2つのアサートを実行するPythonドキュメントでは、外観が少し異なりdef test_shuffleます。
ロブ教会14年

私は同意し、反対します:Dここには明らかに繰り返しがあります:assert multiply(*, *) == *したがって、assert_multiply関数を定義できます。現在のシナリオでは、行数と読みやすさは関係ありませんが、より長いテストでは、複雑なアサーション、フィクスチャ、フィクスチャ生成コードなどを再利用できます。これがベストプラクティスであるかどうかはわかりませんが、通常はこの。
inf3rno 14

10

テストは基本的にコードと同じことを表しているため、重複しているようです

いいえ、これは真実ではありません。

テストには、実装とは異なる目的があります。

  • テストにより、実装が機能することを確認します。
  • これらはドキュメントとして機能します。テストを見ると、コードが満たす必要があるコントラクト、つまり、どの入力がどの出力を返すか、特別なケースは何かなどがわかります。
  • また、テストにより、新しい機能を追加しても既存の機能が破損しないことが保証されます。

4

いいえ。DRYとは、特定のタスクを実行するためのコードを1回だけ記述することです。テストとは、タスクが正しく実行されていることの検証です。投票アルゴリズムに似ているため、同じコードを使用しても意味がありません。


2

DRYの最終的なターゲットには、すべてのテストコードの削除が含まれませんか?

いいえ、DRYの最終的な目標は、実際にはすべての製品コードを排除することです

テストがシステムに必要なものの完全な仕様である場合、対応する製品コード(またはバイナリ)を自動的に生成し、製品コード自体を事実上削除する必要があります。

これは実際に、モデル駆動型アーキテクチャのようなアプローチが達成すると主張するものです。人間が設計した単一の真実のソースであり、そこからすべてが計算によって導き出されます。

私は逆(すべてのテストを取り除く)が望ましいとは思わない:

  • 実装と仕様の間のインピーダンス不整合を解決する必要があります。量産コードは意図をある程度伝えることができますが、明確に表現されたテストほど簡単に推論することはできません。私たち人間は私たちが物事を構築している理由についてのより高い見方を必要としています。DRYのためにテストを行わなくても、とにかく仕様を文書に書き留める必要があります。これは、インピーダンスミスマッチとコードの非同期化に関して、私に尋ねると間違いなく危険な獣です。
  • 実動コードは、おそらく十分な時間をかけて正しい実行可能仕様から簡単に導出できますが、テストスイートはプログラムの最終コードから再構成するのがはるかに困難です。実行時のコードユニット間のやり取りは困難であるため、コードを見ているだけでは仕様が明確に表示されません。これが、テストレスのレガシーアプリケーションを扱うのに苦労している理由です。言い換えれば、アプリケーションを数か月以上存続させたい場合は、テストスイートがあるものよりも、運用コードベースをホストしているハードドライブを失う方が良いでしょう。
  • 本番コードでは、テストコードよりも偶然にバグを導入する方がはるかに簡単です。また、量産コードは自己検証ではないため(Design by Contractまたはより豊富なタイプシステムでアプローチできます)、テストするために外部プログラムが必要であり、リグレッションが発生した場合は警告します。

1

時々自分自身を繰り返すことは大丈夫だからです。これらの原則はいずれも、あらゆる状況で疑問や文脈なしに解釈されることを意図したものではありません。私は時々、ナイーブ(および遅い)バージョンのアルゴリズムに対するテストを作成しました。これはかなり明確なDRY違反ですが、間違いなく有益です。


1

単体テストは、意図しない変更をより困難にすることを目的としているため、意図的な変更をより困難にすることもあります。この事実は確かに乾燥した原則に関連しています。

たとえばMyFunction、本番コードで1か所だけで呼び出される関数があり、20個の単体テストを記述する場合、その関数が呼び出されるコード内に21か所を簡単に作成できます。MyFunction(一部の要件が変更されるため)の署名、セマンティクス、またはその両方を変更する必要がある場合、1つだけではなく21の場所を変更する必要があります。そして、その理由は確かにDRY原則の違反ですMyFunction。同じ関数呼び出しを(少なくとも)21回繰り返しました。

このような場合の正しいアプローチは、テストコードにもDRY原則を適用することです。20個の単体テストを記述するとき、単体テストでの呼び出しをMyFunctionいくつかのヘルパー関数(理想的には1つだけ)にカプセル化します。 20ユニットテスト。理想的には、コード呼び出しのたった2つの場所で終わることになりますMyFunction。1つは実動コードから、もう1つは単体テストからです。したがって、MyFunction後の署名を変更する必要がある場合、テストで変更する場所はわずかしかありません。

「少数の場所」はまだ「1つの場所」(単体テストなしで得られるもの)を超えていますが、単体テストを行うことの利点は、変更するコードが少ないという利点を大きく上回るはずです(そうでなければ、単体テストを完全に実行します)違う)。


0

ソフトウェア構築の最大の課題の1つは、要件を把握することです。それは「このソフトウェアは何をすべきか」という質問に答えることです。 システムに必要なことを正確に定義するには、ソフトウェアに正確な要件が必要ですが、ソフトウェアシステムおよびプロジェクトのニーズを定義する人には、ソフトウェアまたは正式な(数学)背景を持っていない人も含まれます。要件の定義が厳密でないため、ソフトウェア開発はソフトウェアを要件に合わせて検証する方法を見つけることを余儀なくされました。

開発チームは、プロジェクトの口語的な説明をより厳格な要件に翻訳していることに気付きました。テスト分野はソフトウェア開発のチェックポイントとして合体し、顧客が望むと言うことと、ソフトウェアが望むと理解することの間のギャップを埋めます。ソフトウェア開発者と品質/テストチームの両方が(非公式)仕様の理解を形成し、それぞれが(独立して)ソフトウェアまたはテストを作成して、理解が一致することを確認します。(不正確な)要件を理解するために別の人を追加すると、要件の精度をさらに高めるための質問と異なる視点が追加されました。

受け入れテストは常に行われてきたため、テストの役割を拡張して自動化された単体テストを作成するのは自然でした。問題は、テストを行うためにプログラマーを雇うことを意味していたため、品質保証からテストを行うプログラマーに視野を狭めました。

それはすべて、テストが実際のプログラムとほとんど変わらない場合、おそらくテストを間違って行っているということです。Msdyの提案は、テストではなく、テスト方法に重点を置くことです。

皮肉なことに、業界は口語の説明から要件の正式な仕様を取得するのではなく、テストを自動化するコードとしてポイントテストを実装することを選択しています。答えるためにソフトウェアを構築できる正式な要件を作成するのではなく、採用されたアプローチは、正式なロジックを使用してソフトウェアを構築するアプローチではなく、いくつかのポイントをテストすることでした。これは妥協案ですが、かなり効果的で比較的成功しています。


0

テストコードが実装コードにあまりにも似ていると思われる場合、これは、モックフレームワークを使いすぎていることを示している可能性があります。あまりにも低いレベルでの模擬ベースのテストでは、テストのセットアップがテスト対象のメソッドによく似たものになる可能性があります。実装を変更した場合に壊れる可能性の低い高レベルのテストを作成してみてください(これは難しい場合がありますが、それを管理できれば、結果としてより有用なテストスイートが得られます)。


0

既に述べたように、単体テストにはテスト対象のコードの重複を含めないでください。

ただし、セットアップはテスト間で類似している(ただし同一ではない)傾向があるため、ユニットテストは通常​​、「本番」コードほどDRYではありません。特に、かなりの数の依存関係がある場合/フェイク。
もちろん、この種のことを一般的なセットアップ方法(またはセットアップ方法のセット)にリファクタリングすることは可能ですが、これらのセットアップ方法は長いパラメーターリストを持ち、かなり壊れやすい傾向があることがわかりました。

だから実用的である。保守性を損なうことなくセットアップコードを統合できる場合は、必ず実行してください。しかし、代替手段が複雑で脆弱なセットアップ方法のセットである場合、テスト方法を少し繰り返しても問題ありません。

地元のTDD / BDDエバンジェリストは次のように言います。
「本番コードはDRYである必要があります。しかし、テストを「湿らせる」ことは問題ありません。」


0

テストは基本的にコードと同じことを表しているため、コードの(概念ではなく、実装で)重複しているようです。

これは真実ではありません。テストはユースケースを記述し、コードはユースケースを渡すアルゴリズムを記述するため、より一般的です。TDDでは、ユースケース(おそらくユーザーストーリーに基づく)の作成から始め、その後、これらのユースケースを渡すために必要なコードを実装します。そのため、小さなテスト、小さなコードチャンクを作成し、その後、必要に応じてリファクタリングを繰り返してリファクタリングを行います。それが仕組みです。

テストによっても同様に繰り返すことができます。たとえば、フィクスチャ、コードを生成するフィクスチャ、複雑なアサーションなどを再利用できます。通常、テストのバグを防ぐためにこれを行いますが、通常、テストが実際に失敗するかどうかを最初にテストするのを忘れて、 、コードのバグを30分探していて、テストが間違っている場合... xD

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