web-dev-qa-db-ja.com

JUnitやTestNGなどのテストフレームワークは、なぜ「オブジェクト指向」ではないのですか?

「よりオブジェクト指向」とは、つまり、TestNGやJUnitのようなテストフレームワークは、テスターがTestおよびTestSuiteインターフェースの実装を作成するように促すことができるように思えます。 @Testannotationsを使用する現在のアプローチは、多くの場合、多くのメソッドと多くのボイラープレートコードを含む巨大なクラスにつながります。

より "OO"な方法は、たとえば次のようになります。

public class MyTestSuite extends framework.AbstractTestSuite {

  @Override
  public void setup() {
    //...
  }

  @Override
  public void execution() {
    executeInParallel(
      new MyTestA("some param"), 
      new MyTestA("some other param"),
      new MyTestB());
    execute(new MyTestBSubclass());
  }

  @Override
  public void teardown() {
    //...
  }
}

私が予期していないこのアプローチに問題はありますか?

編集:ここでの私の仮定は、framework.Testインターフェースに単一のテストメソッドがあることです。

interface Test {
  void exec();
}

例外がスローされた場合にのみTestが失敗することを理解した上で(または、メソッドがブール値を返す可能性があります)。

JUnit/TestNGについて私が少しおもしろいのは、メソッドが動詞ではなく名詞として扱われているように見えることです。テストとしてインスタンスを使用する場合、コンストラクターのパラメーター化、サブクラス化、構成などの利点を活用できると思います。これらは、テストの種類ごとにクラスを作成するだけでは不十分です。そしてラムダを使用すると、多くの場合、ラムダを作成しなくてもうまくいく可能性があります。

しかし、私が考えていない欠点があるかもしれません...

3
Benjamin Berman

テストフレームワークの基本的な最大の課題は、オブジェクト指向の問題ではないということです。当初、JUnit 1ははるかにオブジェクト指向でした。

  • 一度に実行するすべてのテストオブジェクトを宣言したTestSuiteオブジェクトと、テストを開始するためのコールバックがありました
  • Test基本クラスには、その機能を実行するためにオーバーライドできるsetUp()およびtearDown()仮想メソッドがありました。

本当の問題は個々のテストでした。呼び出す必要のあるすべてのメソッドを宣言する仮想メソッドを作成できますが、フレームワークの有用性が失われ始めます。初日、JUnitは便利にするためにリフレクションを使用する必要がありました。

JUnitには、Word testをテストメソッドの先頭に追加すると、テストとして実行されるという基本的なパターンがありました。典型的なテストクラスは次のようになります。

public class MyBeautifulTest extends Test {
    public void testThisIsATest() {
        Assert.isTrue(true);
    }
}

これは長い間十分に機能していましたが、多くの人がセットアップと破棄に使用された方法の名前について不満を言っていました。彼らは、基本クラスTestを必要とすることで、基本クラスが原因でさまざまなタイプのテスト組織を防止できると不満を述べました。

注釈を入力

結局、Java言語はアノテーション機能とともに登場しました。これは、JUnitに対するすべての不満を回避するための手段でした。TestNGが生まれたのはこのときでした。TestNGは、新しい言語機能を利用して、ユーザーがsetup/teardownメソッドに好きな名前を付けることができるようにします。基本クラスがないため、ユーザーは必要に応じてテストを組み立てることができました。

最終的にJUnitが採用されたのは、それが需要だったからです。 JUnitとTestNGの両方で、適切なアノテーションが付いたテストクラスがスキャンされ、ユーザーがテストスイートを手作業で保守する必要がなくなりました。つまり、少なくとも1つのテストを含む新しいテストクラスを作成するだけで、自動的に取得されます。

Long Story Short

アノテーションと静的メソッドのインポートを使用することで、テストフレームワークの使いやすさが向上しました。また、次のような新しいテスト方法の機会も開かれました。

  • パラメータ化されたテスト(値の範囲内で同じテストを実行するために値を渡す)
  • 理論(アプリケーション全体で「真実」をテストする)
  • レポートを改善するためのテストの分類

これがすべて純粋にOOのように機能する方法を想像できるかもしれませんが、Java言語の設計では、常に妥協が必要になります。リフレクションを使用しないと、これらのテストフレームワークのユーティリティと一致することはできません。他のオブジェクト指向言語には、純粋なOO設計でユーティリティを有効にするさまざまな機能がある場合があります。ただし、 Java(およびC#)には制限があり、注釈、リフレクション、および流動性のあるアサーションは、開発者が維持する必要のある最小限のコードに対して最も有用です。

結局のところ、テストを維持するのが面倒なら、テストは書かれません。多くの開発者にユニットテストを自動化させるのは、すでに困難な戦いです。すべての障壁はそれらをさらにやる気にさせます。

6
Berin Loritsch

あなたが尋ねた

私が予期していないこのアプローチに問題はありますか?

JavaまたはC#のインターフェースは、クラスが実装できる有限の固定数のメソッドのみを提供します。そのため、提案により、リフレクションによって個々のテストを分離することは(不可能ではないにしても)非常に困難になりますたとえば、JUnit GUIは利用可能なテストのリストを表示できませんテストが実行される前に。これらを個別に実行するためのオプションを簡単に提供できませんでした。

2
Doc Brown

JUnitテストフレームワークは、ケントベックのSUnitフレームワーク/パターン[ 1 ] [ 2 ]の直接移植を開始しました。これは、Smalltalk/Agileコミュニティでおよそ10年前に発生しました。 SUnitは個々のテストオブジェクトを備えています。ただし、90年代に利用可能だったJavaに完全に対応しているわけではありません。特に、SUnitはセレクターを使用しますが、JUnitはリフレクションと後のアノテーションを使用してテストケースを検出します。

SUnitのかなりリテラルな移植は、Javaでは次のようになります。まず、TestCaseクラスを定義します。このクラスのインスタンスは、あなたが提案するのと同様に、run()のテストケースです。サブクラスのメソッドを使用して、リフレクションによって解決されるテストケースを作成できます。各TestCaseサブクラスは、fixture–テストケースが実行される環境を表します。このフィクスチャは、setUp()メソッドによって構成されます。

_class TestCase {
  private String selector;

  public TestCase(String selector) {
    this.selector = selector;
  }

  /// Run whatever code you need to get ready for the test to run.
  protected void setUp() { }

  /// Release whatever resources you used for the test.
  protected void tearDown() { }

  /// Run the selected method.
  public final void run() throws Throwable {
    setUp();
    try {
      this.getClass().getMethod(selector).invoke(this);
    } catch (Java.lang.reflect.InvocationTargetException e) {
        throw e.getCause();  // unwrap wrapped exception
    } finally {
      tearDown();
    }
  }

  /// Run the selected method and register the result.
  public final void run(TestResult result) {
    try {
      run();
    } catch(Throwable e) {
      result.error(this, e);
      return;
    }
    result.pass(this);
  }

  /// The base class can define assertions that will be inherited by tests.
  protected void should(boolean result) {
    if (!result) throw new AssertionError("check failed");
  }

  protected void shouldnt(boolean result) {
    if (result) throw new AssertionError("check failed");
  }

  @Override public String toString() {
    return String.format("%s.%s", getClass().getName(), selector);
  }
}
_

TestSuiteはテストケースのコレクションです。

_class TestSuite {
  public final String name;
  private List<TestCase> testCases = new ArrayList<>();

  public TestSuite(String name) {
    this.name = name;
  }

  public TestSuite addTestCase(TestCase testCase) {
    testCases.add(testCase);
    return this;
  }

  public TestResult run() {
    TestResult result = new TestResult(name);
    for (TestCase each: testCases) {
      each.run(result);
    }
    return result;
  }
}

class TestResult {
  public final String name;
  public List<String> errors = new ArrayList<>();
  public List<String> passed = new ArrayList<>();

  public TestResult(String name) {
    this.name = name;
  }

  public void error(TestCase testCase, Throwable error) {
    errors.add(String.format("%s: %s", testCase, error));
  }

  public void pass(TestCase testCase) {
    passed.add(testCase.toString());
  }

  public int count() {
    return passed.size() + errors.size();
  }
}
_

これで、サンプルのテストスイートを実装できます。

_class SetTestCase extends TestCase {
  Set<Object> empty;
  Set<Object> full;

  public SetTestCase(String selector) {
    super(selector);
  }

  @Override protected void setUp() {
    empty = new HashSet<>();
    full = new HashSet<>();
    full.add("abc");
    full.add(5);
  }

  public void testAdd() {
    empty.add(5);
    should(empty.contains(5));
  }

  public void testRemove() {
    full.remove(5);
    should(full.contains("abc"));
    shouldnt(full.contains(5));
  }

  public static TestSuite testSuite() {
    TestSuite suite = new TestSuite("Set Tests");
    suite.addTestCase(new SetTestCase("testAdd"));
    suite.addTestCase(new SetTestCase("testRemove"));
    return suite;
  }

  public static void main(String[] args) {
    TestResult result = testSuite().run();
    System.out.printf("Ran %d test cases\n", result.count());
    for (String error: result.errors) {
      System.out.printf("Test failure: %s\n", error);
    }
    if (!result.errors.isEmpty())
      System.exit(1);
  }
}
_

このアプローチは非常に柔軟です。テストメソッドを直接実行できます(Smalltalkは本質的にインタラクティブな環境でした)。テストスイートをアセンブルするときに、任意のロジックを追加できます。しかし、それにはいくつかの問題のある側面があります:

  • テストケースは名前で解決されます
  • テストケースをスイートに追加手動で–エラーが発生しやすく、面倒です
  • テストケースのフィクスチャは再利用されません
  • テストケースの構築/セットアップは別々のフェーズで行われます
  • javaで1回限りのフィクスチャを作成するのは面倒です

JUnitはこれらの問題のいくつかを修正します。重要なのは、テストケースがリフレクションによって発見され、明示的にリストする必要がないことです。一方、これはある程度の柔軟性を犠牲にします。 TestCaseクラスには複数のケースのコードが含まれているため、フィクスチャでいくつかのテストケースが実行されているフィクスチャではなく、テストケースコレクションとして誤用され続けています。しかし、おそらくsetUp()メソッドは、フィクスチャを管理するための最良のアプローチではありませんか?

この設計ではTestCaseはインターフェイスにはならないことに注意してください。この設計では、メソッド解決、結果管理、およびサブクラスへのアサーションを提供するための配管が提供されるためです。ただし、TestSuiteからrun(TestResult)以外のすべてを非表示にする追加のインターフェイスが存在する可能性があります。

JavaまたはSmalltalkとは異なり、コードの厳密なクラス指向の構造を適用しない言語では、フィクスチャの単純なセットアップではなく、クラスの動作。たとえば、JavaScriptはRspecスタイルのdescribe-itテストを優先します。

_describe('Set', () => {

  it('can add elements', () => {
    const empty = new Set();
    empty.add(5);
    assert(empty.has(5));
  });

  it('can delete elements', () => {
    const full = new Set(["abc", 5]);
    full.delete(5);
    assert(full.has("abc"));
    assert(!full.has(5));
  });

});
_

Javaですべてのテストケースに1つの巨大なメソッドがなければ、それを引き出すことはできません。そして、Java 8の前に文字通りそれを行うことができませんでしたまた、暗黙的にフィクスチャを設定できるなど、いくつかの利点も失われます(このパターンのほとんどの実装はbeforeメソッドを提供します)。

重要なのは、xUnitパターンが普及して以来、多くのことが起こったということです。

  • OOPについてさらに多くのことを学びました。往年のベストプラクティスが常に今日のベストプラクティスであるとは限りません。 OOPが常に適切であるとは限りません。プログラミングに対するJavaのクラス指向のアプローチは制限的であり、より良い代替策を妨げます。JUnitが特定の設計に固執する場合、下位互換性によっても動機付けられます。

  • 自動テストについては、さらに多くのことを学びました。ユニットテストへのxUnitスタイルは、もはや最新のものではありません。要件の取得には適しておらず、プログラマーにとっても便利ではありません。より良いアプローチには、例による仕様やRspecスタイルのテストなどのBDDスタイルの手法、およびHaskellのクイックチェックによるプロパティベースのテストが含まれます。その一部はJavaおよびJUnitでも利用できますが、それでもなんらかの方法でJavaの構造に適合している必要があります。これは、多くの場合、魔法の名前または注釈を持つメソッドを意味します。

JavaとC#は、エレガントなテストをより困難にする唯一の言語ではないことに注意してください。たとえば、Pythonのpytestは、xUnitが夢見るよりもはるかに優れたフィクスチャシステムを備えていますが、ホスト言語(実際のラムダはありません)。私が見つけた最も有用なアプローチの1つは、テストをフレームワークとしてではなくパターンとして扱うことです。標準のテストフレームワークが邪魔になる場合、多くの場合、作成する方が便利ですテストのスタイルに完全に適合する独自のクイックテスト実装。

1
amon