抽象クラスを単体テストする方法:スタブで拡張しますか?


446

抽象クラス、および抽象クラスを拡張するクラスを単体テストする方法を考えていました。

抽象クラスを拡張してテストし、抽象メソッドをスタブ化してから、すべての具象メソッドをテストする必要がありますか?次に、オーバーライドするメソッドのみをテストし、抽象クラスを拡張するオブジェクトのユニットテストで抽象メソッドをテストしますか?

抽象クラスのメソッドをテストするために使用できる抽象テストケースがあり、抽象クラスを拡張するオブジェクトのテストケースでこのクラスを拡張する必要がありますか?

私の抽象クラスにはいくつかの具体的なメソッドがあることに注意してください。

回答:


268

Mockオブジェクトを記述して、テストのためだけに使用します。通常、これらは非常に非常に最小限(抽象クラ​​スから継承)であり、それ以上ではありません。次に、単体テストで、テストする抽象メソッドを呼び出すことができます。

あなたが持っている他のすべてのクラスのようないくつかのロジックを含む抽象クラスをテストする必要があります。


9
くそー、モックを使うという考えに同意したのはこれが初めてだ。
ジョナサンアレン

5
モックとテストの2つのクラスが必要です。モッククラスは、テスト中の抽象クラスの抽象メソッドのみを拡張します。これらのメソッドはテストされないため、no-opやnullを返すことができます。テストクラスは、非抽象パブリックAPI(つまり、抽象クラスによって実装されるインターフェース)のみをテストします。抽象メソッドをカバーしていないため、抽象クラスを拡張するクラスでは、追加のテストクラスが必要になります。
サイバーモンク

10
明らかにこれを行うことは可能ですが、派生クラスを真にテストするには、この基本機能を何度もテストすることになります。これにより、抽象テストフィクスチャが作成され、テストでこの重複を排除できます。これはすべてにおいがする!そもそもなぜ抽象クラスを使用しているのかをもう一度見直して、他に何かうまくいくかどうかを確認することを強くお勧めします。
Nigel Thorne

5
過大評価の次の答えははるかに優れています。
マーティンスパマー

22
@MartiSpamer:この回答は、あなたが以下でより良いと考える回答よりもはるかに早く(2年)書かれたため、過大評価されているとは思いません。パトリックがこの回答を投稿したときの状況では、それは素晴らしかったので、パトリックを奨励しましょう。お互い励ましましょう。乾杯
マーヴィン・トベジャネ

449

抽象基本クラスを使用する方法は2つあります。

  1. あなたは抽象オブジェクトを特化していますが、すべてのクライアントはその基本インターフェイスを通じて派生クラスを使用します。

  2. 抽象基本クラスを使用して、デザイン内のオブジェクト内の重複を除外し、クライアントは独自のインターフェイスを通じて具体的な実装を使用します。


1のソリューション-戦略パターン

オプション1

最初の状況の場合、実際には、派生クラスが実装している抽象クラスの仮想メソッドによって定義されたインターフェースがあります。

これを実際のインターフェースにして、抽象クラスを具象に変更することを検討し、コンストラクタでこのインターフェースのインスタンスを取得する必要があります。派生クラスは、この新しいインターフェースの実装になります。

IMotor

つまり、新しいインターフェースのモックインスタンスを使用して、以前は抽象クラスをテストし、現在はパブリックインターフェースを介して新しい実装をテストできます。すべてがシンプルでテスト可能です。


2のソリューション

2番目の状況がある場合、抽象クラスはヘルパークラスとして機能しています。

AbstractHelper

含まれている機能を見てください。この重複を最小限に抑えるために、操作されているオブジェクトにプッシュできるかどうかを確認してください。まだ何か残っている場合は、それをヘルパークラスにして、具体的な実装がコンストラクターで受け取り、基本クラスを削除することを検討してください。

モーターヘルパー

これもまた、シンプルで簡単にテストできる具象クラスにつながります。


原則として

複雑なオブジェクトの単純なネットワークよりも、単純なオブジェクトの複雑なネットワークを優先します。

拡張可能なテスト可能なコードの鍵は、小さなビルディングブロックと独立した配線です。


更新:両方の混合を処理する方法?

これらの両方の役割を実行する基本クラスを持つことは可能です...つまり:パブリックインターフェイスを持ち、ヘルパーメソッドを保護しています。この場合は、ヘルパーメソッドを1つのクラス(scenario2)に分解して、継承ツリーを戦略パターンに変換できます。

基本クラスが直接実装するメソッドがあり、他のメソッドが仮想であることがわかった場合でも、継承ツリーを戦略パターンに変換できますが、責任が正しく調整されておらず、リファクタリングが必要です。


Update 2:飛び石としての抽象クラス(2014/06/12)

先日、アブストラクトを使っていたので、その理由を探っていきたいです。

構成ファイルの標準形式があります。この特定のツールには、すべてその形式の3つの構成ファイルがあります。各設定ファイルに強く型付けされたクラスが必要だったので、依存性注入により、クラスはそれが必要とする設定を要求できました。

これを実装するには、設定ファイルの形式を解析する方法を知っている抽象基本クラスと、同じメソッドを公開するが、設定ファイルの場所をカプセル化する派生クラスを用意しました。

3つのクラスがラップする「SettingsFileParser」を作成し、基本クラスに委任してデータアクセスメソッドを公開することもできます。他の何よりも多くの委任コードを含む3つの派生クラスにつながるため、私はまだこれを行わないことを選択しました。

しかし...このコードが進化し、これらの各設定クラスのコンシューマーがより明確になるにつれて。各設定ユーザーは、いくつかの設定を要求し、それらを何らかの方法で変換します(設定はテキストであるため、数値に変換するオブジェクトにラップする場合があります)。これが発生すると、このロジックをデータ操作メソッドに抽出し、厳密に型指定された設定クラスにプッシュし直します。これにより、設定の各セットのより高いレベルのインターフェースにつながり、最終的には「設定」を処理していることに気付かなくなります。

この時点で、強く型付けされた設定クラスは、基になる「設定」実装を公開する「getter」メソッドを必要としなくなります。

その時点で、私はもうパブリックインターフェイスに設定アクセサーメソッドを含めたくありません。したがって、このクラスを変更して、設定パーサークラスを派生させるのではなくカプセル化します。

したがって、Abstractクラスは次のとおりです。現時点で委任コードを回避する方法と、後でデザインを変更するように通知するコード内のマーカー。私はそれに到達できないかもしれないので、それは良い人生を送るかもしれません...コードだけが伝えることができます。

これは、「静的メソッドなし」や「プライベートメソッドなし」などのルールに当てはまることがわかります。それらはコードのにおいを示しています...そしてそれは良いことです。それはあなたが見逃してしまった抽象化を探し続ける...そして、あなたがその間に顧客に価値を提供し続けることを可能にします。

このようなルールは、メンテナンス可能なコードが谷にあるランドスケープを定義していると思います。新しい動作を追加すると、コードに雨が降るようなものです。最初は着地する場所に配置します。次に、リファクタリングを行って、優れた設計の力がすべての谷に到達するまで動作を押し進めます。


18
これは素晴らしい答えです。トップ定格よりもはるかに良い。しかし、私は本当にテスト可能なコードを書きたいと思う人だけがそれを感謝するでしょう.. :)
MalcomTucker

22
これが実際にどれほど良い答えであるか、私は乗り越えることができません。抽象クラスについての私の考え方が完全に変わりました。ナイジェルに感謝します。
MalcomTucker、2012年

4
ああ..考え直さなければならないもう一つの教義!感謝(今のところ皮肉なことに、かつ一度皮肉なことに私はそれを同化し、より優れたプログラマーのように感じています)
Martin Lyne

11
いい答えだ。確かに何か考えなければならないことですが...あなたが言っていることは、基本的に抽象クラスを使用しないように煮詰めていませんか?
brianestey 2013年

32
ルールのみの+1、「複雑なオブジェクトの単純なネットワークよりも単純なオブジェクトの複雑なネットワークを優先する」
David Glass

12

抽象クラスとインターフェイスに対して私が行うことは次のとおりです。具体的にオブジェクトを使用するテストを記述します。ただし、タイプX(Xは抽象クラス)の変数はテストで設定されていません。このテストクラスはテストスイートには追加されませんが、そのサブクラスであり、変数をXの具体的な実装に設定するsetupメソッドを持っています。このようにして、テストコードを複製しません。未使用のテストのサブクラスは、必要に応じてテストメソッドを追加できます。


これはサブクラスでキャストの問題を引き起こしませんか?Xにメソッドaがあり、YがXを継承するが、メソッドbも持つ場合。テストクラスをサブクラス化するとき、bでテストを実行するために、抽象変数をYにキャストする必要はありませんか?
Johnno Nolan

8

抽象クラスで具体的にユニットテストを行うには、テスト目的でユニットテストを派生させ、base.method()の結果と継承時の意図した動作をテストする必要があります。

メソッドを呼び出すことでテストするので、実装して抽象クラスをテストします...


8

抽象クラスにビジネス価値のある具体的な機能が含まれている場合は、通常、抽象データをスタブするテストダブルを作成するか、モックフレームワークを使用してこれを直接テストします。どちらを選択するかは、抽象メソッドのテスト固有の実装を作成する必要があるかどうかに大きく依存します。

これを行う必要がある最も一般的なシナリオは、テンプレートメソッドパターンを使用している場合です。、サードパーティが使用する拡張可能なフレームワークを構築しているときなどいるときです。この場合、抽象クラスは、テストするアルゴリズムを定義するものであるため、特定の実装よりも抽象ベースをテストする方が理にかなっています。

ただし、これらのテストでは、実際のビジネスロジックの具体的な実装のみに焦点を当てることが重要であると思います。抽象クラスの実装の詳細を単体テストしないでください。脆弱なテストが発生するためです。


6

1つの方法は、抽象クラスに対応する抽象テストケースを記述してから、抽象テストケースをサブクラス化する具体的なテストケースを記述することです。これを、元の抽象クラスの具象サブクラスごとに実行します(つまり、テストケース階層はクラス階層を反映しています)。junit recipiesブックのインターフェースのテストを参照してください:http ://safari.informit.com/9781932394238/ch02lev1sec6

xUnitパターンのTestcaseスーパークラスも参照してください:http ://xunitpatterns.com/Testcase%20Superclass.html


4

私は「抽象的な」テストに反対するでしょう。テストは具体的なアイデアであり、抽象化されていないと思います。共通の要素がある場合は、それらをヘルパーメソッドまたはクラスに配置して、全員が使用できるようにします。

抽象テストクラスのテストについては、それが何をテストしているのかを自問してください。いくつかのアプローチがあり、シナリオで何が機能するかを確認する必要があります。サブクラスで新しいメソッドをテストしようとしていますか?次に、テストがそのメソッドとのみ対話するようにします。基本クラスのメソッドをテストしていますか?次に、おそらくそのクラス専用の個別のフィクスチャを用意し、必要なだけ多くのテストを行って、各メソッドを個別にテストします。


すでにテストしたコードを再テストしたくなかったので、抽象的なテストケースをたどっていました。抽象クラスのすべての具象メソッドを1か所でテストしようとしています。
Paul Whelan、

7
ヘルパークラスに共通の要素を抽出することには、少なくとも一部の(多くの場合)には同意しません。抽象クラスに具体的な機能が含まれている場合、その機能を直接ユニットテストすることは完全に許容できると思います。
Seth Petry-Johnson、

4

これは、抽象クラスをテストするためのハーネスを設定するときに私が通常従うパターンです。

public abstract class MyBase{
  /*...*/
  public abstract void VoidMethod(object param1);
  public abstract object MethodWithReturn(object param1);
  /*,,,*/
}

そして、私がテストで使用するバージョン:

public class MyBaseHarness : MyBase{
  /*...*/
  public Action<object> VoidMethodFunction;
  public override void VoidMethod(object param1){
    VoidMethodFunction(param1);
  }
  public Func<object, object> MethodWithReturnFunction;
  public override object MethodWithReturn(object param1){
    return MethodWihtReturnFunction(param1);
  }
  /*,,,*/
}

予期しないときに抽象メソッドが呼び出されると、テストは失敗します。テストをアレンジするとき、アサートを実行したり、例外をスローしたり、さまざまな値を返したりするラムダを使用して、抽象メソッドを簡単に作成できます。


3

具象メソッドが抽象メソッドのいずれかを呼び出す場合、その戦略は機能せず、各子クラスの動作を個別にテストする必要があります。それ以外の場合は、抽象クラスの具象メソッドが子クラスから分離されている場合は、拡張して、説明したとおりに抽象メソッドをスタブ化することで問題ありません。


2

抽象クラスの基本機能をテストしたいと思うかもしれませんが、メソッドをオーバーライドせずにクラスを拡張し、抽象メソッドを最小限の労力で模倣するのが最善でしょう。


2

抽象クラスを使用する主な動機の1つは、アプリケーション内でポリモーフィズムを有効にすることです。つまり、実行時に別のバージョンに置き換えることができます。実際、これはインターフェースを使用することとほとんど同じですが、抽象クラスがいくつかの一般的な配管を提供する点が異なります。これは、しばしばテンプレートパターンと呼ばれます

単体テストの観点から、考慮すべき2つの点があります。

  1. 抽象クラスとそれに関連するクラスの相互作用。抽象クラスが他のクラスとうまく機能することが示されているため、模擬テストフレームワークの使用はこのシナリオに最適です。

  2. 派生クラスの機能。派生クラス用に作成したカスタムロジックがある場合は、それらのクラスを個別にテストする必要があります。

編集:RhinoMocksは、クラスから動的に派生させることで実行時にモックオブジェクトを生成できる素晴らしいモックテストフレームワークです。このアプローチにより、派生クラスを手作業でコーディングする時間を無数に節約できます。


2

まず、抽象クラスに具体的なメソッドが含まれている場合、この例を考慮してこれを行う必要があると思います

 public abstract class A 

 {

    public boolean method 1
    {
        // concrete method which we have to test.

    }


 }


 class B extends class A

 {

      @override
      public boolean method 1
      {
        // override same method as above.

      }


 } 


  class Test_A 

  {

    private static B b;  // reference object of the class B

    @Before
    public void init()

      {

      b = new B ();    

      }

     @Test
     public void Test_method 1

       {

       b.method 1; // use some assertion statements.

       }

   }

1

抽象クラスが実装に適している場合は、(上記のように)派生した具象クラスをテストします。あなたの仮定は正しいです。

将来の混乱を避けるために、この具象テストクラスはモックではなく、偽物であることに注意してください。

厳密に言うと、モックは次の特性によって定義されます。

  • テストされるサブジェクトクラスのすべての依存関係の代わりにモックが使用されます。
  • モックはインターフェースの疑似実装です(一般的に、依存関係はインターフェースとして宣言する必要があることを思い出してください。テストのしやすさはこのための1つの主な理由です)
  • モックのインターフェイスメンバーの動作(メソッドでもプロパティでも)は、テスト時に(ここでもモックフレームワークを使用して)提供されます。このようにして、テストされる実装とその依存関係の実装(すべて独自の個別のテストが必要)の結合を回避します。

1

@ patrick-desjardinsの回答に続いて@Test、次のように抽象クラスとその実装クラスを実装しました。

抽象クラス-ABC.java

import java.util.ArrayList;
import java.util.List;

public abstract class ABC {

    abstract String sayHello();

    public List<String> getList() {
        final List<String> defaultList = new ArrayList<>();
        defaultList.add("abstract class");
        return defaultList;
    }
}

抽象クラスはインスタンス化することはできませんが、彼らはサブクラス化することができ、具体的なクラスDEF.javaは次のように、次のとおりです。

public class DEF extends ABC {

    @Override
    public String sayHello() {
        return "Hello!";
    }
}

抽象メソッドと非抽象メソッドの両方をテストする@Testクラス:

import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;

import org.junit.Test;

public class DEFTest {

    private DEF def;

    @Before
    public void setup() {
        def = new DEF();
    }

    @Test
    public void add(){
        String result = def.sayHello();
        assertThat(result, is(equalTo("Hello!")));
    }

    @Test
    public void getList(){
        List<String> result = def.getList();
        assertThat((Collection<String>) result, is(not(empty())));
        assertThat(result, contains("abstract class"));
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.