テスト対象のクラスの一部を偽造しても大丈夫ですか?


22

クラスがあるとしましょう(不自然な例とその悪いデザインを許してください):

class MyProfit
{
  public decimal GetNewYorkRevenue();
  public decimal GetNewYorkExpenses();
  public decimal GetNewYorkProfit();

  public decimal GetMiamiRevenue();
  public decimal GetMiamiExpenses();
  public decimal GetMiamiProfit();

  public bool BothCitiesProfitable();

}

(GetxxxRevenue()およびGetxxxExpenses()メソッドには、スタブ化された依存関係があることに注意してください)

現在、GetNewYorkProfit()とGetMiamiProfit()に依存するBothCitiesProfitable()を単体テストしています。GetNewYorkProfit()とGetMiamiProfit()をスタブしても大丈夫ですか?

そうしないと、GetNewYorkProfit()とGetMiamiProfit()をBothCitiesProfitable()と同時にテストしているようです。GetxxxProfit()メソッドが正しい値を返すように、GetxxxRevenue()およびGetxxxExpenses()のスタブを設定する必要があります。

これまでのところ、内部メソッドではなく外部クラスのスタブの依存関係の例を見てきました。

そして、それが大丈夫な場合、これを行うために使用すべき特定のパターンはありますか?

更新

コアの問題を見逃しているのではないかと心配しています。これはおそらく私の貧弱な例のせいです。基本的な質問は、クラス内のメソッドが、その同じクラス内で公開されている別のメソッドに依存している場合、その他のメソッドをスタブ化しても大丈夫(または推奨)ですか?

たぶん何かが欠けているかもしれませんが、クラスを分割することが常に意味があるかどうかはわかりません。おそらく、もう1つの最小限の良い例は次のとおりです。

class Person
{
 public string FirstName()
 public string LastName()
 public string FullName()
}

ここで、フルネームは次のように定義されます:

public string FullName()
{
  return FirstName() + " " + LastName();
}

FullName()をテストするときにFirstName()とLastName()をスタブしても大丈夫ですか?


いくつかのコードを同時にテストする場合、これはどのように悪いのでしょうか?どうしたの?通常、コードをより頻繁に、より集中的にテストすることをお勧めします。
ユーザー不明

あなたの更新について知りました、私は私の答えを更新しました
ウィンストン・エワート

回答:


27

問題のクラスを分割する必要があります。

各クラスはいくつかの簡単なタスクを実行する必要があります。タスクが複雑すぎてテストできない場合、クラスが行うタスクは大きすぎます。

この設計の愚かさを無視する:

class NewYork
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}


class Miami
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}

class MyProfit
{
     MyProfit(NewYork new_york, Miami miami);
     boolean bothProfitable();
}

更新

クラスのスタブメソッドの問題は、カプセル化に違反していることです。テストでは、オブジェクトの外部動作が仕様に一致するかどうかを確認する必要があります。オブジェクト内で何が起こっても、そのビジネスは何もありません。

FullNameがFirstNameとLastNameを使用するという事実は、実装の詳細です。クラス外では、それが真実であることを気にするべきではありません。オブジェクトをテストするためにパブリックメソッドをモックすることにより、そのオブジェクトが実装されていることを仮定しています。

将来のある時点で、その仮定は正しくなくなるかもしれません。おそらく、すべての名前ロジックは、Personが単に呼び出すNameオブジェクトに再配置されます。おそらくFullNameは、FirstNameとLastNameを呼び出すのではなく、メンバー変数first_nameとlast_nameに直接アクセスします。

2番目の質問は、なぜそうする必要があると感じるかです。結局、個人クラスは次のようにテストできます。

Person person = new Person("John", "Doe");
Test.AssertEquals(person.FullName(), "John Doe");

この例では、スタブの必要性を感じる必要はありません。あなたがそうするなら、あなたはスタブ幸せで、まあ...それを止めてください!とにかくメソッドの内容を制御できるので、そこでメソッドをモックするメリットはありません。

FullNameが使用するメソッドがモックされるのが理にかなっていると思われる唯一のケースは、なんとかFirstName()とLastName()が自明でない操作である場合です。たぶん、あなたはそれらのランダムな名前ジェネレータの1つを書いているか、FirstNameとLastNameが答えなどをデータベースに問い合わせています。しかし、それが起こっている場合は、オブジェクトがPersonクラスに属さない何かをしていることを示唆しています。

別の言い方をすれば、メソッドのモックとは、オブジェクトを取得して2つの部分に分割することです。片方がモックされ、もう片方がテストされています。あなたがしていることは、本質的にオブジェクトのアドホックな分割です。その場合は、オブジェクトを既に分割してください。

クラスが単純な場合、テスト中にクラスの一部をモックアウトする必要性を感じるべきではありません。クラスが複雑で、モックする必要があると感じる場合は、クラスをより単純な部分に分割する必要があります。

もう一度更新

私の見方では、オブジェクトには外部と内部の振る舞いがあります。外部の動作には、他のオブジェクトへの戻り値の呼び出しなどが含まれます。明らかに、そのカテゴリ内のすべてのものをテストする必要があります。(そうでなければ、何をテストしますか?)しかし、内部の動作は実際にテストされるべきではありません。

ここで、内部動作がテストされます。これは、外部動作の結果になるためです。しかし、私は、内部の動作を直接テストするのではなく、外部の動作を介して間接的にテストを作成します。

何かをテストしたい場合は、外部の動作になるように移動する必要があると考えています。そのため、何かをモックしたい場合は、オブジェクトを分割して、モックしたいものが問題のオブジェクトの外部動作に収まるようにする必要があると思います。

しかし、それはどのような違いをもたらしますか?FirstName()とLastName()が別のオブジェクトのメンバーである場合、FullName()の問題は本当に変わりますか?FirstNameをモックする必要があると判断した場合、LastNameは別のオブジェクトに存在するのに実際に役立ちますか?

モッキングアプローチを使用すると、オブジェクトに継ぎ目が作成されると思います。外部データソースと直接通信するFirstName()やLastName()などの関数があります。また、ないFullName()もあります。しかし、それらはすべて同じクラスに属しているため、明らかではありません。データソースに直接アクセスすることを想定していないものとそうでないものがあります。これらの2つのグループを分割するだけで、コードはより明確になります。

編集

一歩下がって質問してみましょう。テストするときにオブジェクトをモックするのはなぜですか?

  1. テストを一貫して実行する(実行ごとに変わるものへのアクセスを避ける)
  2. 高価なリソースへのアクセスを避けます(サードパーティのサービスなどにアクセスしないでください)
  3. テスト対象のシステムを簡素化する
  4. 考えられるすべてのシナリオ(つまり、障害のシミュレーションなど)をテストしやすくします。
  5. 他のコード部分の変更がこのテストを壊さないように、他のコード部分の詳細に依存することは避けてください。

さて、このシナリオには理由1〜4は当てはまらないと思います。fullnameをテストするときに外部ソースをモックすると、モッキングのこれらすべての理由が処理されます。処理されない唯一の要素は単純さですが、オブジェクトは十分に単純であるため、心配する必要はありません。

あなたの懸念は理由番号5だと思います。懸念は、将来のある時点でFirstNameとLastNameの実装を変更するとテストが中断されることです。将来、FirstNameとLastNameは別の場所またはソースから名前を取得する可能性があります。ただし、FullNameは常にになりますFirstName() + " " + LastName()。そのため、FirstNameとLastNameをモックしてFullNameをテストする必要があります。

その場合、他のオブジェクトよりも変更される可能性が高い人物オブジェクトのサブセットがあります。オブジェクトの残りはこのサブセットを使用します。そのサブセットは現在、1つのソースを使用してデータを取得していますが、後日、まったく異なる方法でそのデータを取得する場合があります。しかし、私にとっては、そのサブセットは抜け出そうとする別個のオブジェクトのように思えます。

オブジェクトのメソッドをモックすると、オブジェクトを分割しているように思えます。しかし、あなたはアドホックな方法でそうしています。コードでは、Personオブジェクト内に2つの異なる部分があることを明確にしません。したがって、実際のコードでそのオブジェクトを単純に分割し、コードを読んで何が起こっているかが明確になるようにします。意味のあるオブジェクトの実際の分割を選択し、テストごとにオブジェクトを別々に分割しようとしないでください。

オブジェクトを分割することに反対するかもしれませんが、なぜですか?

編集

私は間違っていた。

個々のメソッドをモックしてアドホック分割を導入するのではなく、オブジェクトを分割する必要があります。ただし、オブジェクトを分割する1つの方法に集中しすぎました。ただし、オブジェクト指向はオブジェクトを分割する複数の方法を提供します。

私が提案するもの:

class PersonBase
{
      abstract sring FirstName();
      abstract string LastName();

      string FullName()
      {
            return FirstName() + " " + LastName();
      }
 }

 class Person extends PersonBase
 {
      string FirstName(); 
      string LastName();
 }

 class FakePerson extends PersonBase
 {
      void setFirstName(string);
      void setLastName(string);
      string getFirstName();
      string getLastName();
 }

多分それはあなたがずっとやっていたことです。しかし、各メソッドがどちらの側にあるのかを明確に描いているため、このメソッドにはモックメソッドで見た問題はないと思います。また、継承を使用することで、追加のラッパーオブジェクトを使用した場合に生じる厄介な問題を回避できます。

これは多少の複雑さをもたらしますが、いくつかのユーティリティ関数については、基礎となるサードパーティのソースをモックするだけでテストするでしょう。確かに、彼らは破壊の危険性が高まっていますが、再配置する価値はありません。分割する必要があるほど複雑なオブジェクトがある場合、このようなものは良い考えだと思います。


8
よく言った。テストで回避策を見つけようとしている場合、おそらくデザインを再検討する必要があります(補足として、フランクシナトラの声が頭に残っています)。
問題のある

2
「オブジェクトをテストするためにパブリックメソッドをモックすることにより、そのオブジェクトが実装される[方法]についての仮定を立てています。」しかし、オブジェクトをスタブするときはいつでもそうではありませんか?たとえば、メソッドxyz()が他のオブジェクトを呼び出すことがわかっているとします。xyz()を単独でテストするには、他のオブジェクトをスタブ化する必要があります。したがって、私のテストは、xyz()メソッドの実装の詳細を知っている必要があります。
ユーザー

私の場合、「FirstName()」および「LastName()」メソッドは単純ですが、サードパーティのAPIに結果を照会します。
ユーザー

@User、更新、おそらくあなたはFirstNameとLastNameをテストするためにサードパーティのAPIをモックしているでしょう。FullNameをテストするときに同じモックを行うことの何が問題になっていますか?
ウィンストンイーバート

@Winston、それは私の全体のポイントのようなものです。現在、fullname()をテストするためにfirstnameとlastnameで使用されているサードパーティのAPIをモックしていますが、fullname(もちろん名と姓をテストしているときは大丈夫です)。したがって、名と姓のモックについての私の質問です。
ユーザー

10

Winston Ewertの答えには同意しますが、時間の制約、他の場所で既に使用されているAPI、またはあなたが持っているもののために、クラスを分割することが不可能な場合があります。

そのような場合、メソッドをモックする代わりに、クラスを段階的にカバーするテストを作成します。テストするgetExpenses()getRevenue()、テスト、方法をgetProfit()、方法を、共有メソッドをテスト。特定のメソッドをカバーするテストが複数あることは確かですが、メソッドを個別にカバーする合格テストを書いたので、テストされた入力に対して出力が信頼できることを確認できます。


1
同意した。しかし、これを読んでいる人にとってポイントを強調するために、これはあなたがより良い解決策を行うことができない場合に使用する解決策です。
ウィンストンイーバート

5
@Winston、これがより良い解決策を得るに使用する解決策です。つまり、レガシーコードの大きなボールがある場合、リファクタリングする前に単体テストでカバーする必要があります。
ペテルトレック

@PéterTörök、良い点。それは「より良い解決策はできない」でカバーされていると思いますが。:)
ウィンストンイーバート

@all getProfit()メソッドのテストは、getExpenses()とgetRevenues()の異なるシナリオがgetProfit()に必要なシナリオを実際に増やすため、非常に困難になります。getExpenses()とRevenues()を個別にテストしている場合getProfit()をテストするためにこれらのメソッドをモックすることは良い考えではありませんか?
モハドファリド

7

さらに例を単純化するために、テストしていると言います C()に依存し、A()そしてB()自分自身の依存関係を持って、それぞれが、。IMOは、テストが達成しようとしているものに帰着します。

C()特定の既知の動作の動作をテストしている場合A()し、B()それはおそらくアウトスタブに最も簡単で最良であるA()B()。これはおそらく、純粋主義者による単体テストと呼ばれるでしょう。

システム全体の動作をテストする場合(から C()の視点)を、私は去るだろうA()B()としては実装されており、それらの依存関係アウトのいずれかのスタブや、テストとして、サンドボックス環境をセットアップし(それはテストに可能な場合)データベース。これを統合テストと呼びます。

どちらのアプローチも有効である可能性があります。したがって、どちらの方法でも事前にテストできるように設計されている場合は、長期的にはおそらく改善されるでしょう。

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