web-dev-qa-db-ja.com

単体テストを容易にするために、内部ソフトウェアのクラスですべてのパブリックメソッドに対してプライベートメソッドを使用する価値

これは、データをループして重複排除する、私が作成したクラスのスケルトンです。これはC#にありますが、質問の原則は言語固有ではありません。

public static void DedupeFile(FileContents fc)
{
    BuildNameKeys(fc);
    SetExactDuplicates(fc);
    FuzzyMatching(fc);
}

// algorithm to calculate fuzzy similarity between surname strings
public static bool SurnameMatch(string surname1, string surname2)

// algorithm to calculate fuzzy similarity between forename strings
public static bool ForenameMatch(string forename1, string forename2)

// algorithm to calculate fuzzy similarity between title strings
public static bool TitleMatch(string title1, string title2)

// used by above fn to recognise that "Mr" isn't the same as "Ms" etc
public static bool MrAndMrs(string title1, string title2)

// gives each row a unique key based on name
public static void BuildNameKeys(FileContents fc)

// loops round data to find exact duplicates 
public static void SetExactDuplicates(FileContents fc)

// threads looping round file to find fuzzy duplicates
public static void FuzzyMatching(FileContents fc, int maxParallels = 32)

現在、実際の使用では、最初の関数のみが実際にパブリックである必要があります。残りはすべてこのクラス内でのみ使用され、他では使用されません。

厳密に言うと、もちろんそれらはプライベートであるべきです。ただし、ユニットテストを簡単にするために公開しました。一部の人々は、私がパブリックインターフェイスを介してそれらをテストするべきだと私に言うのは間違いありませんが、それが私がこのクラスを選んだ理由の1つです。これは、そのアプローチが厄介な場所の優れた例です。ファジーマッチング関数は単体テストの優れた候補ですが、その単一の「パブリック」関数のテストはほとんど役に立ちません。

このクラスは、このオフィスの小さなチームの外で使用されることはありません。他のメソッドをプライベートにすることによって与えられる構造的理解が、プライベートメソッドに直接アクセスするためのコードでテストをパックするという特別な価値があるとは思いません。

この「すべてのパブリック」アプローチは、内部ソフトウェアのクラスにとって妥当ですか?または、より良いアプローチはありますか?

プライベートメソッドのユニットテストをどのように行うのですか? に関する質問があることは承知していますが、この質問は、単にメソッドをパブリックのままにすることを優先して、これらの手法をバイパスする価値があるシナリオがあるかどうかについてです。

編集:興味のある方のために、このクラスの再構築は見逃すには学習の機会としては良すぎると思われたため、 CodeReviewSEの完全なコード を追加しました。

30
Bob Tway

ユニットテストを簡単にするために公開しました

書き込みこれらのテストの容易さ。しかし、そのクラスとその内部の仕組みと相互作用する一連のテストを密に結合します。その結果、テストが不安定になります。コードを変更するとすぐにテストが壊れる可能性があります。これは実際のメンテナンスの頭痛を引き起こし、多くの場合、テストの価値よりもトラブルが発生したときにテストを削除するだけです。

ユニットテストは、「コードの可能な限り小さな部分をテストする」ことを意味しないことに注意してください。これは機能ユニットのテストです。つまり、システムの一部への入力のセットに対して、これらの結果が期待されます。その単位は、静的メソッド、クラス、またはアセンブリ内の一連のクラスの場合があります。パブリックAPIのみを対象とすることで、システムの動作を模倣するため、テストの結合性が低下し、堅牢性が高まります。

したがって、メソッドをプライベートにして、「 'FileContents'全体のDTO」をモックし、1つの真のパブリックメソッドのみをテストします。最初はより多くの作業が必要になりますが、時間の経過とともに、このような有用なテストを作成することの利点を享受するでしょう。

64
David Arno

私は通常、パブリックインターフェースを介してプライベートメンバー機能を実行することを期待します。この例では、さまざまなファイルコンテキストをフィードするためのさまざまなテストを記述し、それらのメソッドを実行するためにさまざまなデータセットを用意します。

私はあなたのテストがそれらのプライベートメソッドについて知っているべきではないと思います。私はそれらが実装の一部であると思います、そしてあなたのテストはそれがどのように実装されたかではなく、あなたのコンポーネントがどのように機能するかに関係するべきです。

このクラスは、このオフィスの小さなチームの外では使用されません。

公開するとすぐに再利用されることに驚くかもしれません。もう1つの方法は、パブリックにすると再利用されることに同意し、メソッドを一般的なユーティリティクラスに引き出すことです。こうすることで、これらのメソッドを個別にテストできます(公開したため)。これはパブリックユーティリティクラスであるため、これらがthisシナリオでのみ使用されることを暗黙的に想定していません。現在注目されています。

37
Brian Agnew

問題はデザインにあると思います。私の直感は、コードの後に​​テストを記述したか、テストの記述を開始したときにすでに完全な実装を念頭に置いていて、デザインに合うようにテストを実行したと言います。

このタイプの問題は、小さなアサーションを作成し、それを可能な限り単純な方法で通過させるという短いTDDサイクルを覚えておくことで回避できると思います。

パブリックメソッドを介してすべてのプライベートメソッドを実行するのは難しいとおっしゃっています。これはおそらく、クラスが多すぎることを示しています。テストの後にテストを追加して、要件が満たされていることを確認し、それを保守可能で読み取り可能なコードにリファクタリングするだけでは構築できない場合は、エンティティを実装するための十分な知識がないか、複雑すぎます(ええええ、それは一般化です、私は彼らが悪いことを知っています)。

メソッド名を見ると、このクラスには多くの責任があるように見えます。いくつかのアルゴリズムの実装があり、スレッドを管理し、ディスクからファイルを読み取ることさえ可能です。作業を管理可能な再利用可能なチャンクに分割します。マッチング/検証アルゴリズムは簡単に注入でき(戦略として、より好ましくはimoとして、デリゲートとして)、スレッド管理はおそらくより高いレベルで発生するはずです。

何十億もの責任を持つ(多かれ少なかれ)大規模で複雑なクラスがなくなったら、テストはほとんど簡単になります。

9
sara

@BrianAgnewと@kaiに同意しますが、コメント以上のものを追加したいと思います。

IDedupeFiler(または何でも)はそのパブリックインターフェイスを介してテストする必要がありますが、OPは個々のサブルーチンをテストすることに価値があると判断しました。ファイルサイズや行数(クラスの責任の大まかなプロキシカウントにすぎません)に関係なく、OPは、このクラスの上からテストするには複雑すぎるため、決定しました。

ちなみに、これは良いことです。TDDのような人々がテストを行う必要があるため、コーダーは設計を適応(および改善)する必要があります。以前のテストは、このプロセスが発生する早い段階で記述されていることを指摘するのは有効ですが、OPはテスト不可能な設計決定の道のりではなく、リファクタリングは面倒ではありません。

問題は、OPが(1)メソッドを公開して、リファクタリングを減らしてテスト容易性を実現するか、それとも他のことを行うべきかどうかですI @kaiに同意し、OPは(2)このクラスを分離され、個別にテスト可能なチャンクに分割する必要があると言います

(1)カプセル化を解除し、クラスの使用とパブリックインターフェースが実際に何であるかをすぐに明確にしないようにします。 OPはこれが彼らの質問の中で最良ではないことを認めていると思いますOOP(2)はより多くのクラスを意味しますが、それは多くの問題ではないと思います、そしてそれを提供します設計上の妥協のないテスト容易性。

サブメソッドが個別に個別にテスト可能な懸念を表すということに本当に同意できない場合は、クラスを調べてそれらをテストしないでください。 最上位のパブリックメソッドを使用して実行します。これがどれほど難しいかは、これが正しい選択であったかどうかの良い指標になります。

6
Nathan Cooper

単体テストの見方を少し変えることでメリットを得られると思います。それらをallコードが機能していることを保証する方法と考えるのではなく、パブリックインターフェイスが要求どおりに機能することを保証する方法と考えてください。

言い換えれば、内部のテストについてはまったく心配しないでください。クラスXに入力を与えると、Y出力が得られることを証明する単体テストを作成してください-それだけです。それをどうやって管理するかは全く問題ではありません。実際、privateメソッドは完全に間違っている、役に立たない、冗長であるなどの可能性があり、パブリックインターフェイスが本来の機能を実行している限り、単体テストの観点からは機能しています。

新しいライブラリが出てきて後でうまくいくときにコードに戻ってリファクタリングできるようにしたい、または不要な作業を行っていたと気づいたり、名前の付け方や整理方法を決定したりするので、これは重要ですより明確にすることができます。テストがパブリックインターフェースのみを対象としている場合は、問題が発生することを心配することなく、自由にテストを実行できます。

単体テストについての考え方をこのように変えて、特定のクラスのテストを書くのが依然として難しいことがわかった場合は、設計にいくつかの改善が必要であることを示しています。たぶんクラスはより少ないことをしようとするべきでしょう-おそらくそれらのプライベートメソッドのいくつかは本当に新しい小さなクラスに属しています。または、外部依存関係を管理するために依存関係注入を使用する必要があるかもしれません。

この場合、この重複排除をどのように行っているかについての詳細がわからないので、パブリックにしたいメソッドは、実際には個別のクラスとして、またはユーティリティライブラリのパブリックメソッドとして適していると思います。このようにして、許可された入力と予想される出力の範囲とともに、それぞれのインターフェイスを定義し、重複排除スクリプト自体から分離してそれらをテストできます。

3
thomij

他の回答に加えて、これらの関数をプライベートにして、パブリックインターフェイスを介してテストすることには別の利点があります。

コードのコードカバレッジメトリックを収集すると、関数が使用されなくなった時期がわかりやすくなります。ただし、これらのすべての関数をパブリックにしてユニットテストを作成すると、他に何もしない場合でも、少なくともユニットテストでそれらを呼び出すことができます。

この例を考えてみましょう:

public void foo() {
    bar();
    baz();
}

public void bar() { ... }

public void baz() { ... }

次に、foobar、およびbazの個別の単体テストを行います。したがって、これらはすべてユニットテストフレームワークによって呼び出され、すべてが使用されていることを示すコードカバレッジを取得します。

次に、この変更を検討します。

public void foo() {
    bar();
}

public void bar() { ... }

public void baz() { ... }

単体テストではこれらの関数がすべて呼び出されるので、コードカバレッジはそれらがすべて使用されていると言います。しかし、bazは、単体テスト以外のソフトウェアの他の部分から呼び出されなくなりました。それは事実上デッドコードです。

次のように書きましたか:

public void foo() {
    bar();
}

private void bar() { ... }

private void baz() { ... }

次に、bazが変更されると、コードカバレッジfooが100%失われ、それがコードからコードを削除するシグナルになります。または、これはbazが実際には非常に重要な操作であるために赤信号を発生させ、その省略は他の問題を引き起こします(うまくいけば、他の単体テストがそれをキャッチしますが、おそらくそうではありません)。

3
Shaz

関数とテストケースを分離して、クラスを分離します。 1つは関数を含み、もう1つは関数を呼び出し、結果が期待したものと等しいかどうかをアサートするテストケースを含みます。私はCではなく、Javaでこれを行うためにJunitを使用します。これにより、ロジックが分割され、クラスが読みやすくなります。また、問題を心配する必要もありません。

0
bish