web-dev-qa-db-ja.com

TDD Red-Green-Refactorと、プライベートになるメソッドをテストするかどうか/方法

私が理解している限り、ほとんどの人は、プライベートメソッドを直接テストするのではなく、パブリックメソッドで呼び出す必要があることに同意しているようです。彼らの要点はわかりますが、「TDDの3つの法則」に従い、「赤-緑-リファクタリング」サイクルを使用しようとすると、いくつか問題があります。私はそれが例によって最もよく説明されると思います:

現在、ファイル(タブ区切りデータを含む)を読み取り、非数値データを含むすべての列をフィルターで除外できるプログラムが必要です。おそらくこれを行うための簡単なツールがすでにいくつかあると思いますが、私は自分でゼロから実装することにしました。これは、TDDを実践するためのすてきでクリーンなプロジェクトになると考えたからです。

したがって、最初に、「Red Hatを搭載」します。つまり、失敗するテストが必要です。私は考えました、行内のすべての非数値フィールドを見つけるメソッドが必要です。したがって、単純なテストを作成しますが、もちろんすぐにはコンパイルできません。そのため、関数自体の作成を開始し、数サイクル前後(赤/緑)で、機能する関数と完全なテストを作成します。

次に、ファイルを1行ずつ読み取り、「findNonNumericFields」関数を各行で呼び出して、最終的に削除する必要があるすべての列を収集する「gatherNonNumericColumns」関数を続行します。いくつかの赤と緑のサイクルで、作業関数と完全なテストが完了しました。

今、私はリファクタリングする必要があると思います。私のメソッド "findNonNumericFields"は、 "gatherNonNumericColumns"を実装するときに必要になると考えたためにのみ設計されたため、 "findNonNumericFields"をプライベートにすると合理的だと思われます。ただし、テストしているメソッドにアクセスできなくなるため、最初のテストは失敗します。

そのため、私はプライベートメソッドと、それをテストする一連のテストで終わることになります。多くの人がプライベートメソッドはテストすべきではないとアドバイスしているので、私はここの隅に自分を描いたように感じます。しかし、どこで失敗したのですか?

私はより高いレベルから始めて、最終的に私のパブリックメソッドになるテスト(つまり、findAndFilterOutAllNonNumericalColumns)を書いていた可能性がありますが、それはTDDの全体的なポイントとは少々相反するように感じます(ボブおじさんによると)。 :テストの作成と本番用コードを絶えず切り替える必要があり、いつでも、すべてのテストが1分以内に機能することを確認してください。パブリックメソッドのテストを作成することから始めると、プライベートメソッドのすべての詳細が機能するようになるまでに数分(場合によっては数時間、場合によっては数日)かかり、テストがパブリックメソッドをテストするようになります。メソッドがパスします。

じゃあ何をすればいいの? TDD(迅速な赤-緑-リファクターサイクル)は、プライベートメソッドと互換性がないのですか?それとも私のデザインに欠陥がありますか?

93
Henrik Berg

単位

問題がどこから始まったかを正確に特定できると思います。

私は考えました、行内のすべての非数値フィールドを見つけるメソッドが必要です。

これは、「gatherNonNumericColumnsへの個別のテスト可能なユニットですか、それとも同じユニットの一部ですか?」

答えが "yes、Separate"の場合、アクションは簡単です。そのメソッドは適切なクラスでパブリックである必要があるため、1つのユニットとしてテストできます。あなたの考え方は次のようなものです「1つの方法をテストする必要があり、別の方法もテストする必要があります」

しかし、あなたが言ったことから、答えは "いいえ、同じものの一部"であることがわかりました。この時点で、findNonNumericFieldsを完全に記述してテストし、次に_gatherNonNumericColumnsを記述して、計画を立てる必要はなくなります。代わりに、単にgatherNonNumericColumnsと書く必要があります。とりあえず、findNonNumericFieldsは、次の赤いテストケースを選択してリファクタリングを行う際に考えている宛先の一部である可能性があります。今回はあなたの考え方は「1つのメソッドをテストする必要があります。テストする間、完成した実装にはおそらくこの別のメソッドが含まれることを覚えておいてください。」


短いサイクルを保つ

上記を行うと、最後から2番目の段落で説明する問題が発生するはずですnot

パブリックメソッドのテストを作成することから始めると、プライベートメソッドのすべての詳細が機能するようになるまでに数分(場合によっては数時間、場合によっては数日)かかり、テストがパブリックメソッドをテストするようになります。メソッドがパスします。

このテクニックでは、findNonNumericFields全体を最初から実装した場合にのみ緑になるテストを記述する必要はありません。多くの場合、findNonNumericFieldsは、テストしているパブリックメソッドのインラインコードとして開始します。これは、いくつかのサイクルの過程で構築され、最終的にはリファクタリング中に抽出されます。


ロードマップ

この特定の例のおおよそのロードマップを提供するために、使用した正確なテストケースはわかりませんが、パブリックメソッドとしてgatherNonNumericColumnsを記述していたとしましょう。次に、テストケースは、findNonNumericFieldsに対して作成したものと同じである可能性が高く、それぞれ1行のみのテーブルを使用します。その1行のシナリオが完全に実装され、メソッドを抽出するように強制するテストを記述したい場合は、反復を追加する必要がある2行のケースを記述します。

44
Ben Aaronson

ユニットテストはメソッドベースであると多くの人が考えています。そうではありません。意味のある最小単位をベースにする必要があります。ほとんどの場合、これはクラスがエンティティ全体としてテストする必要があるものであることを意味します。個々のメソッドではありません。

これで明らかにクラスのメソッドを呼び出すことになりますが、テストは持っているブラックボックスオブジェクトに適用するものと考える必要があります。これにより、クラスが提供する論理演算を確認できるはずです。これらはテストする必要があるものです。クラスが大きすぎて論理演算が複雑すぎる場合は、最初に修正する必要のある設計上の問題があります。

千のメソッドを持つクラスはテスト可能と思われるかもしれませんが、各メソッドを個別にテストするだけでは、実際にはクラスをテストしていません。一部のクラスは、メソッドが呼び出される前に特定の状態である必要がある場合があります。たとえば、データを送信する前に接続を設定する必要があるネットワーククラスなどです。データ送信メソッドは、クラス全体から独立して考えることはできません。

したがって、プライベートメソッドはテストとは無関係であることがわかります。クラスのパブリックインターフェイスを呼び出してプライベートメソッドを実行できない場合、それらのプライベートメソッドは役に立たず、いずれにしても使用されません。

多くの人がプライベートメソッドをテスト可能なユニットに変換しようとするのは、それらのテストを実行するのは簡単に見えるためですが、これではテストの粒度がかかりすぎます。 Martin Fowlerさんのコメント

私はユニットがクラスであるという概念から始めますが、私はしばしば密接に関連するクラスの束を取り、それらを単一のユニットとして扱います

これはオブジェクト指向システムにとって非常に理にかなっており、オブジェクトはユニットとして設計されています。個々のメソッドをテストする場合は、Cなどの手続き型システムを作成するか、代わりに完全に静的関数で構成されるクラスを作成する必要があります。

66
gbjbaanb

データ収集メソッドがテストに値するほど複雑であるという事実およびは、いくつかの一部ではなく独自のメソッドになるように主な目標から十分に分離されていますループはソリューションを指します:これらのメソッドを作成しますnotプライベートですが、収集/フィルタリング/集計を提供するいくつかのotherクラスのメンバー機能性。

次に、ヘルパークラスのダムデータを変更する側面(たとえば、「文字と数字を区別する」)のテストを1か所で記述し、主要な目標(たとえば、「売上高を取得する」)のテストを別の場所で記述します。通常のビジネスロジックのテストでは、基本的なフィルタリングテストを繰り返す必要はありません。

かなり一般的に、あることを行うクラスに、その主な目的に必要であるがそれとは別のことを行うための広範なコードが含まれている場合、そのコードは別のクラスに存在し、パブリックメソッドを介して呼び出されます。 誤ってのみがそのコードを含むクラスのプライベートコーナーで非表示にしないでください。これにより、テスト容易性と理解可能性が同時に向上します。

51
Kilian Foth

個人的には、テストを書いたとき、実装の考え方に遠くまで行ったように感じます。あなた仮定特定の方法が必要になるでしょう。しかし、あなたは本当にクラスがすることになっていることをするためにそれらを必要としますか?誰かがやって来て内部でリファクタリングした場合、クラスは失敗しますか?もしあなたがsingクラスであったなら(そしてそれは私の意見ではテスターの考え方であるはずです)、数値をチェックする明示的なメソッドがあれば本当に気にする必要はありません。

クラスのパブリックインターフェイスをテストする必要があります。プライベート実装は、理由によりプライベートです。必要がなく、変更できるため、パブリックインターフェイスの一部ではありません。実装の詳細です。

パブリックインターフェイスに対してテストを作成した場合、実際に遭遇した問題が発生することはありません。プライベートメソッド(素晴らしい)をカバーするパブリックインターフェイスのテストケースを作成することも、作成することもできません。その場合、プライベートメソッドについて一生懸命考え、いずれにしても到達できない場合は、それらをすべて破棄するときがきたかもしれません。

29
nvoigt

クラスが内部で行うと予想されることに基づいてTDDを実行しません。

テストケースは、クラス/機能/プログラムが外部の世界に対して何をしなければならないかに基づいている必要があります。あなたの例では、ユーザーはto find all the non-numerical fields in a line?

答えが「いいえ」の場合、そもそも書くのは悪いテストです。機能のテストを記述したいクラス/インターフェイスでレベル-「これを機能させるためにクラスメソッドが実装する必要があるもの」レベルではなく、テストです。

TDDのフローは次のとおりです。

  • 赤(クラス/オブジェクト/関数/その他が外界に対して何をしているのか)
  • 緑(この外部ワールド関数を機能させるための最小限のコードを記述)
  • リファクタリング(これを機能させるためのより良いコードは何か)

「将来的にプライベートメソッドとしてXが必要になるので、最初に実装してテストさせてください」というわけではありません。あなたがこれをしていることに気づいたら、あなたは誤って「赤い」段階を行っています。これはあなたの問題のようです。

プライベートメソッドになるメソッドのテストを頻繁に記述している場合は、いくつかのことの1つを行っています。

  • インターフェース/パブリックレベルのユースケースを正しく理解していないため、それらのテストを書くことができない
  • 設計を劇的に変更し、いくつかのテストをリファクタリングする(機能が新しいテストでテストされるかどうかによっては、これは良いことかもしれません)
11
enderland

テスト全般に関して、よくある誤解が生じています。

テストに慣れていないほとんどの人は、次のように考え始めます。

  • 関数Fのテストを書く
  • fを実装する
  • 関数Gのテストを書く
  • fの呼び出しを使用してGを実装する
  • 関数Hのテストを書く
  • gの呼び出しを使用してHを実装する

等々。

ここでの問題は、実際には関数Hの単体テストがないことです。Hをテストすることになっているテストは、実際にはH、G、Fを同時にテストしています。

これを解決するには、テスト可能なユニットが相互に依存することはなく、インターフェースに依存する必要があることを理解する必要があります。あなたの場合、ユニットが単純な関数である場合、インターフェースは単なる呼び出しシグネチャです。したがって、Fと同じシグネチャを持つany関数で使用できるように、Gを実装する必要があります。

これをどのように行うことができるかは、プログラミング言語によって異なります。多くの言語では、関数(またはそれらへのポインタ)を引数として他の関数に渡すことができます。これにより、各機能を個別にテストできます。

9
initcrash

テスト駆動開発中に作成するテストは、クラスがそのパブリックAPIを正しく実装していることを確認すると同時に、そのパブリックAPIのテストと使用が簡単であることを確認するものです。

必ずプライベートメソッドを使用してそのAPIを実装できますが、TDDを介してテストを作成する必要はありません。パブリックAPIが正しく機能するため、機能はテストされます。

ここで、プライベートメソッドが非常に複雑でスタンドアロンテストに値すると仮定しますが、元のクラスのパブリックAPIの一部としては意味がありません。まあ、これはおそらく、それらが実際には他のクラスのパブリックメソッドである必要があることを意味します。元のクラスが独自の実装で利用するクラスです。

パブリックAPIをテストするだけで、実装の詳細を後で簡単に変更できるようになります。役に立たないテストは、あなたが発見したばかりの洗練されたリファクタリングをサポートするためにそれらを書き直す必要がある場合にのみあなたを困らせるでしょう。

8
Bill Michell

正しい答えは、パブリックメソッドから始めることについての結論です。まず、そのメソッドを呼び出すテストを作成します。失敗するので、その名前で何もしないメソッドを作成します。次に、戻り値をチェックするテストを正しく行うことができます。

(私はあなたの関数が何をするかについて完全に明確ではありません。それは数値以外の値を取り除いたファイル内容を含む文字列を返しますか?)

メソッドが文字列を返す場合は、その戻り値を確認します。だからあなたはそれを構築し続けます。

プライベートメソッドで発生することはすべて、プロセス中のある時点でパブリックメソッドに含め、リファクタリング手順の一部としてのみプライベートメソッドに移動する必要があると思います。私の知る限り、リファクタリングはテストに失敗する必要はありません。機能を追加するときに必要なのは、失敗したテストだけです。リファクタリング後にテストを実行して、すべてが成功することを確認する必要があります。

4
Matt Dyer

ここの隅に自分を描いたような気がします。しかし、どこで失敗したのですか?

古い格言があります。

計画に失敗すると、失敗することになります。

TDDをするときは、ただ座ってテストを書くだけで、デザインは魔法のように起こると人々は考えているようです。これは真実ではありません。高レベルの計画を立てる必要があります。最初にインターフェース(パブリックAPI)を設計すると、TDDから最良の結果が得られることがわかりました。個人的には、最初にクラスを定義する実際のinterfaceを作成します。

gaspテストを書く前に「コード」を書きました!うーん、ダメ。私はしませんでした。従うべき契約設計を書きました。方眼紙にUML図を書き留めることで、同様の結果が得られると思います。ポイントは、あなたには計画がなければならないということです。 TDDは、コードの一部を無造作にハッキングするライセンスではありません。

「Test First」は誤称だと本当に感じます。 Design Firstthenテスト。

もちろん、あなたのコードからより多くのクラスを抽出することについて他の人が与えたアドバイスに従ってください。クラスの内部をテストする必要性を強く感じている場合は、それらの内部を簡単にテストできるユニットに抽出し、それを注入します。

3
RubberDuck

テストもリファクタリングできることを忘れないでください!メソッドをプライベートにすると、パブリックAPIが減るので、その「失われた機能」に対応するいくつかのテストを破棄しても問題はありません(別名、複雑さが軽減されます)。

他の人は、あなたのプライベートメソッドは他のAPIテストの一部として呼び出されるか、到達できないため削除できると言っています。実際、実行パスについて考えると、物事はよりきめ細かくなります。

たとえば、除算を実行するパブリックメソッドがある場合、ゼロによる除算が発生するパスをテストすることができます。メソッドをプライベートにすると、選択肢が得られます。ゼロによる除算のパスを考慮することができますor他のメソッドによってどのように呼び出されるかを考慮することによって、そのパスを削除できます。

このようにして、いくつかのテスト(例えば、ゼロ除算)を破棄し、残りのパブリックAPIに関して他のテストをリファクタリングすることができます。もちろん、理想的な世界では、既存のテストがこれらすべての残りのパスを処理しますが、現実は常に妥協です;)

2
Warbo

プライベートメソッドを別のクラスのパブリックメソッドにできる場合があります。

たとえば、スレッドセーフではなく、クラスを一時的な状態にしておくプライベートメソッドがある場合があります。これらのメソッドは、最初のクラスによってプライベートに保持される別のクラスに移動できます。したがって、クラスがQueueの場合、パブリックメソッドを持つInternalQueueクラスを作成でき、QueueクラスはInternalQueueインスタンスをプライベートに保持します。これにより、内部キューをテストできます。また、InternalQueueの個々の操作が何であるかが明確になります。

(これは、Listクラスがないと想像し、List関数を使用するクラスのプライベートメソッドとしてList関数を実装しようとした場合に最も明白です。)

2
Thomas Andrews

私はこれを経験し、あなたの痛みを感じました。

私の解決策は:

モノリスを構築するようなテストの扱いを停止します。

テストのセットを作成したら、たとえば5と言って、いくつかの機能を実行するこれらすべてのテストを維持する必要はありません(特に、これが他の一部になる場合)。

たとえば、私はよく持っています:

  • 低レベルテスト1
  • それを満たすためのコード
  • 低レベルテスト2
  • それを満たすためのコード
  • 低レベルテスト3
  • それを満たすためのコード
  • 低レベルテスト4
  • それを満たすためのコード
  • 低レベルテスト5
  • それを満たすためのコード

それで私は持っています

  • 低レベルテスト1
  • 低レベルテスト2
  • 低レベルテスト3
  • 低レベルテスト4
  • 低レベルテスト5

しかし、それを呼び出すより高いレベルの関数を追加すると、多くのテストが行​​われますmightこれらの低レベルのテストを次のように減らすことができます:

  • 低レベルのテスト1
  • 低レベルテスト5

悪魔は詳細にあり、それを行う能力は状況に依存します。

0
Michael Durrant

なぜあなたの言語には完全に公的なものと完全に私的なものの2つのレベルのプライバシーしかないのでしょうか。

非公開メソッドをパッケージからアクセスできるようにすることはできますか?次に、テストを同じパッケージに入れて、パブリックインターフェイスの一部ではない内部の仕組みをテストします。ビルドシステムは、リリースバイナリをビルドするときにテストを除外します。

もちろん、場合によっては、定義クラス以外にはアクセスできない真にプライベートなメソッドが必要になります。私はそのようなすべての方法が非常に小さいことを望みます。一般に、メソッドを小さく(20行未満など)維持すると、テスト、メンテナンス、およびコードの理解だけが容易になります。

0
9000

私は時々、プロテクトにプライベートメソッドをぶつけて、より詳細なテスト(公開されたパブリックAPIよりもタイト)を可能にしました。これは規則ではなく(できれば非常にまれな)例外である必要がありますが、特定の場合に遭遇する可能性があります。また、これは、パブリックAPIを構築するときにまったく考慮したくないものです。まれな状況で内部使用ソフトウェアで使用できる「チート」の詳細です。

0
Brian Knoblauch