抽象クラス、および抽象クラスを拡張するクラスを単体テストする方法を考えていました。
抽象クラスを拡張し、抽象メソッドをスタブアウトしてから、抽象クラスをテストしてから、すべての具象メソッドをテストする必要がありますか?次に、オーバーライドするメソッドのみをテストし、抽象クラスを拡張するオブジェクトの単体テストで抽象メソッドをテストしますか?
抽象クラスのメソッドをテストするために使用できる抽象テストケースを用意し、抽象クラスを拡張するオブジェクトのテストケースでこのクラスを拡張する必要がありますか?
私の抽象クラスにはいくつかの具体的なメソッドがあることに注意してください。
Mockオブジェクトを作成し、テストのためだけに使用します。通常、これらは非常に非常に最小限(抽象クラスから継承)であり、それ以上ではありません。その後、ユニットテストで、テストする抽象メソッドを呼び出すことができます。
あなたが持っている他のすべてのクラスのようないくつかのロジックを含む抽象クラスをテストする必要があります。
抽象基本クラスを使用する方法は2つあります。
抽象オブジェクトを特化していますが、すべてのクライアントはベースインターフェイスを介して派生クラスを使用します。
抽象基本クラスを使用して設計内のオブジェクト内の重複を除外し、クライアントは独自のインターフェイスを介して具体的な実装を使用します。
Solution For 1-Strategy Pattern
最初の状況がある場合、実際には、派生クラスが実装している抽象クラスの仮想メソッドによって定義されたインターフェイスがあります。
これを実際のインターフェースにして、抽象クラスを具体的に変更し、コンストラクターでこのインターフェースのインスタンスを取得することを検討してください。派生クラスは、この新しいインターフェイスの実装になります。
これは、新しいインターフェイスのモックインスタンスを使用して、以前は抽象クラスをテストでき、現在はパブリックインターフェイスを介して新しい実装をそれぞれテストできることを意味します。すべてがシンプルでテスト可能です。
2のソリューション
2番目の状況がある場合、抽象クラスはヘルパークラスとして機能しています。
含まれている機能をご覧ください。この重複を最小限に抑えるために、操作中のオブジェクトにプッシュできるかどうかを確認してください。まだ何か残っている場合は、具象実装がコンストラクターで使用し、基本クラスを削除するヘルパークラスにすることを検討してください。
これもまた、シンプルで簡単にテストできる具体的なクラスにつながります。
ルールとして
複雑なオブジェクトの単純なネットワークよりも単純なオブジェクトの複雑なネットワークを優先します。
拡張可能なテスト可能なコードの鍵は、小さなビルディングブロックと独立した配線です。
更新:両方の混合を処理する方法?
これらの両方の役割を実行する基本クラスを持つことが可能です。つまり、パブリックインターフェイスを持ち、ヘルパーメソッドを保護しています。この場合、ヘルパーメソッドを1つのクラス(scenario2)に分解し、継承ツリーを戦略パターンに変換できます。
基本クラスが直接実装するメソッドがあり、他のメソッドが仮想であることがわかった場合、継承ツリーを戦略パターンに変換できますが、責任が正しく調整されておらず、リファクタリングが必要です。
更新2:飛び石としての抽象クラス(2014/06/12)
先日、アブストラクトを使用した状況があったので、その理由を調べたいと思います。
構成ファイルには標準形式があります。この特定のツールには、すべてその形式の3つの構成ファイルがあります。各設定ファイルに厳密に型指定されたクラスが必要だったので、依存関係の注入を通じて、クラスはそれが管理する設定を要求できました。
設定ファイルの形式を解析する方法を知っている抽象基本クラスと、同じメソッドを公開するが設定ファイルの場所をカプセル化した派生クラスを使用して、これを実装しました。
3つのクラスがラップする「SettingsFileParser」を作成し、データアクセスメソッドを公開するために基本クラスに委任することもできます。私はこれを行わないことを選択しましたまだ、より多くの委任他の何よりもそれらのコード。
ただし...このコードが進化し、これらの各設定クラスのコンシューマーがより明確になるにつれて。各設定ユーザーは、いくつかの設定を要求し、何らかの方法で変換します(設定はテキストであるため、数値に変換するオブジェクトなどにラップする場合があります)。これが発生すると、このロジックをデータ操作メソッドに抽出し、強く型付けされた設定クラスにプッシュし始めます。これにより、設定の各セットに対してより高いレベルのインターフェイスが使用され、最終的には「設定」を処理していることに気づきません。
この時点で、強く型付けされた設定クラスは、基になる「設定」実装を公開する「getter」メソッドを必要としなくなります。
その時点で、パブリックインターフェイスに設定アクセサメソッドを含めないようになりました。そのため、このクラスを派生させずに設定パーサークラスをカプセル化するように変更します。
したがって、Abstractクラスは、現時点では委任コードを回避するための方法であり、後でデザインを変更することを思い出させるコード内のマーカーです。私はそれに到達することは決してないかもしれないので、しばらくは生き続けるかもしれません...コードだけが伝えることができます。
これは、「静的メソッドなし」や「プライベートメソッドなし」などのルールに当てはまります。コードの匂いを示しています...それは良いことです。それはあなたが見逃している抽象化を探し続ける...そしてその間に顧客に価値を提供し続けることを可能にします。
保守可能なコードが谷間に存在するランドスケープを定義するこのようなルールを想像します。新しい動作を追加すると、コードに雨が降るようなものになります。最初は、どこに着地してもそれを配置します。その後、リファクタリングして、優れたデザインの力がすべての行動を谷間で終わるまで押し進めます。
私が抽象クラスと抽象インターフェースに対して行うことは次のとおりです。具体的にオブジェクトを使用するテストを作成します。ただし、タイプX(Xは抽象クラス)の変数はテストでは設定されません。このテストクラスはテストスイートに追加されるのではなく、変数をXの具体的な実装に設定するsetup-methodを持つサブクラスになります。そのようにして、テストコードを複製しません。未使用のテストのサブクラスは、必要に応じてさらにテストメソッドを追加できます。
抽象クラスにビジネス価値のある具体的な機能が含まれている場合、通常、抽象データをスタブ化するテストダブルを作成するか、モックフレームワークを使用してこれを直接テストします。どちらを選択するかは、抽象メソッドのテスト固有の実装を記述する必要があるかどうかに大きく依存します。
これを行う必要がある最も一般的なシナリオは、サードパーティが使用する何らかの拡張可能なフレームワークを構築している場合など、 Template Method pattern を使用している場合です。この場合、抽象クラスは、テストするアルゴリズムを定義するものであるため、特定の実装よりも抽象ベースをテストする方が理にかなっています。
ただし、これらのテストでは、実際のビジネスロジックの具体的な実装のみに焦点を当てることが重要だと思います。抽象クラスのユニットテスト実装の詳細を行うべきではありません。これは、テストが不安定になるためです。
抽象クラスで具体的に単体テストを行うには、テスト目的でユニットを派生させ、base.method()の結果と継承時の意図した動作をテストする必要があります。
メソッドを呼び出すことでテストするので、実装することで抽象クラスをテストします...
1つの方法は、抽象クラスに対応する抽象テストケースを作成してから、抽象テストケースをサブクラス化する具体的なテストケースを作成することです。元の抽象クラスの具体的なサブクラスごとにこれを行います(つまり、テストケースの階層はクラスの階層を反映しています)。 junitレシピ帳のインターフェイスのテストを参照してください: http://safari.informit.com/9781932394238/ch02lev1sec6 。
xUnitパターンのテストケーススーパークラスも参照してください: http://xunitpatterns.com/Testcase%20Superclass.html
私は「抽象的な」テストに反対します。テストは具体的なアイデアであり、抽象化されていないと思います。共通の要素がある場合は、誰でも使用できるようにヘルパーメソッドまたはクラスに配置します。
抽象テストクラスのテストについては、テスト対象を自問してください。いくつかのアプローチがあり、シナリオで機能するものを見つける必要があります。サブクラスで新しいメソッドをテストしようとしていますか?次に、テストがそのメソッドとのみ対話するようにします。基本クラスのメソッドをテストしていますか?次に、おそらくそのクラス専用のフィクスチャを個別に用意し、必要な数のテストで各メソッドを個別にテストします。
これは、抽象クラスをテストするためのハーネスを設定するときに通常従うパターンです。
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);
}
/*,,,*/
}
予期しないときに抽象メソッドが呼び出されると、テストは失敗します。テストを配置するとき、アサートを実行したり、例外をスローしたり、異なる値を返したりするラムダを持つ抽象メソッドを簡単にスタブできます。
具体的なメソッドがいずれかの抽象メソッドを呼び出す場合、その戦略は機能せず、各子クラスの動作を個別にテストする必要があります。それ以外の場合は、抽象クラスの具象メソッドが子クラスから分離されている場合は、前述のように拡張して抽象メソッドをスタブ化しても問題ありません。
抽象クラスを使用する主な動機の1つは、アプリケーション内でポリモーフィズムを有効にすることです。つまり、実行時に別のバージョンに置き換えることができます。実際、これは、抽象クラスがTemplate patternと呼ばれる一般的な配管を提供することを除いて、インターフェイスを使用するのとほとんど同じです。
単体テストの観点から、考慮すべきことが2つあります。
抽象クラスとそれに関連するクラスとの相互作用。模擬テストフレームワークの使用は、抽象クラスが他のクラスとうまく機能することを示しているため、このシナリオに最適です。
派生クラスの機能。派生クラス用に作成したカスタムロジックがある場合は、それらのクラスを個別にテストする必要があります。
編集:RhinoMocksは、クラスから動的に派生することにより、実行時にモックオブジェクトを生成できる素晴らしいモックテストフレームワークです。このアプローチにより、派生クラスを手作業でコーディングする無数の時間を節約できます。
まず、抽象クラスに具体的なメソッドが含まれている場合、この例を考慮してこれを行う必要があると思います
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.
}
}
抽象クラスの基本機能をテストしたいと思うかもしれません...しかし、メソッドをオーバーライドせずにクラスを拡張し、抽象メソッドのモックを最小限に抑えることで、おそらく最善の結果が得られるでしょう。
@ 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"));
}
}
抽象クラスが実装に適している場合、派生した具象クラスをテストします(上記で提案したとおり)。あなたの仮定は正しいです。
将来の混乱を避けるために、この具体的なテストクラスはモックではなく、fakeであることに注意してください。
厳密に言えば、モックは次の特性によって定義されます。