web-dev-qa-db-ja.com

テスト中のクラスの一部を偽って大丈夫ですか?

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

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()と同時にテストしているようです。 GetxxxRevenue()およびGetxxxExpenses()のスタブをセットアップして、GetxxxProfit()メソッドが正しい値を返すようにする必要があります。

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

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

[〜#〜]更新[〜#〜]

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

多分私は何かが足りないかもしれませんが、クラスを分割することが常に意味があるかどうかはわかりません。おそらく別の最小限のより良い例は:

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

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

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

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

22
User

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

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

このデザインの間抜けさを無視して:

_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つの部分に分割することです。 1つの部分は模擬され、もう1つの部分はテストされています。あなたがしていることは、本質的にはオブジェクトのその場限りの解体です。その場合は、オブジェクトを分割してください。

クラスが単純であれば、テスト中にクラスの一部を模擬する必要性を感じてはいけません。クラスが複雑でモックする必要があると感じる場合は、クラスをより単純な部分に分割する必要があります。

再度更新

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

これで、内部動作がテストされます。これは、外部動作が発生するためです。しかし、内部の振る舞いを直接テストするのではなく、外部の振る舞いを介して間接的にテストを書きます。

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

しかし、どのような違いがありますか? 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つの方法に過度に集中しました。ただし、OOは、オブジェクトを分割する複数の方法を提供します。

私が提案するもの:

_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();
 }
_

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

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

27
Winston Ewert

Winston Ewertの回答には同意しますが、時間の制約、他の場所ですでに使用されているAPI、または何が原因であれ、クラスを分割できない場合があります。

そのような場合、メソッドをモックするのではなく、クラスを段階的にカバーするテストを作成します。 getExpenses()メソッドとgetRevenue()メソッドをテストしてから、getProfit()メソッドをテストしてから、共有メソッドをテストします。特定のメソッドをカバーするテストが複数あることは事実ですが、メソッドを個別にカバーするテストに合格したので、テストされた入力があれば、出力の信頼性が保証されます。

11
Problematic

例をさらに簡略化するために、C()A()に依存するB()をテストするとします。これらはそれぞれ独自の依存関係があります。 IMOそれはあなたのテストが達成しようとしていることに帰着します。

C()A()の既知の動作を前提としてB()の動作をテストする場合は、A()B()。これはおそらく純粋主義者によるユニットテストと呼ばれるでしょう。

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

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

7
Rebecca Scott