23

意図

私は次のものを探しています:

  • 堅実な単体テスト方法論
    1. 私のアプローチには何が欠けていますか?
    2. 私は何を間違っていますか?
    3. 私は何をしているのですか?
  • できるだけ多くを自動的に行う方法

現在の環境

  • IDEとしてのEclipse
  • Eclipse に統合されたテスト フレームワークとしてのJUnit
  • アサーションの読みやすさを向上させるための「マッチャー」ライブラリとしてのHamcrest
  • 前提条件検証のためのGoogle Guava

現在のアプローチ

構造

  • テストするクラスごとに 1 つのテスト クラス
  • ネストされた静的クラスにグループ化されたメソッド テスト
  • テストされた動作と期待される結果を指定するためのテスト メソッドの命名
  • メソッド名ではなく、 Java Annotationによって指定された予期される例外

方法論

  • null値に注意
  • 空のList<E>に注意してください
  • 空の文字列に注意してください
  • 空の配列に注意してください
  • コードによって変更されたオブジェクト状態の不変条件に注意してください (事後条件)
  • メソッドは、文書化されたパラメーターの型を受け入れます
  • 境界チェック (例: Integer.MAX_VALUEなど)
  • 特定の型による不変性の文書化 (例: Google Guava ImmutableList<E> )
  • ...これのリストはありますか?あると便利なテスト リストの例:
    • データベース プロジェクトでチェックする項目 (例: CRUD、接続性、ロギングなど)
    • マルチスレッドコードで確認すること
    • EJB の確認事項
    • ... ?

サンプルコード

これは、いくつかのテクニックを示すための不自然な例です。


MyPath.java

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.Arrays;
import com.google.common.collect.ImmutableList;
public class MyPath {
  public static final MyPath ROOT = MyPath.ofComponents("ROOT");
  public static final String SEPARATOR = "/";
  public static MyPath ofComponents(String... components) {
    checkNotNull(components);
    checkArgument(components.length > 0);
    checkArgument(!Arrays.asList(components).contains(""));
    return new MyPath(components);
  }
  private final ImmutableList<String> components;
  private MyPath(String[] components) {
    this.components = ImmutableList.copyOf(components);
  }
  public ImmutableList<String> getComponents() {
    return components;
  }
  @Override
  public String toString() {
    StringBuilder stringBuilder = new StringBuilder();
    for (String pathComponent : components) {
      stringBuilder.append("/" + pathComponent);
    }
    return stringBuilder.toString();
  }
}

MyPathTests.java

import static org.hamcrest.Matchers.is;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.hamcrest.collection.IsEmptyCollection.empty;
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.junit.Assert.assertThat;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import com.google.common.base.Joiner;
@RunWith(Enclosed.class)
public class MyPathTests {
  public static class GetComponents {
    @Test
    public void componentsCorrespondToFactoryArguments() {
      String[] components = { "Test1", "Test2", "Test3" };
      MyPath myPath = MyPath.ofComponents(components);
      assertThat(myPath.getComponents(), contains(components));
    }
  }
  public static class OfComponents {
    @Test
    public void acceptsArrayOfComponents() {
      MyPath.ofComponents("Test1", "Test2", "Test3");
    }
    @Test
    public void acceptsSingleComponent() {
      MyPath.ofComponents("Test1");
    }
    @Test(expected = IllegalArgumentException.class)
    public void emptyStringVarArgsThrows() {
      MyPath.ofComponents(new String[] { });
    }
    @Test(expected = NullPointerException.class)
    public void nullStringVarArgsThrows() {
      MyPath.ofComponents((String[]) null);
    }
    @Test(expected = IllegalArgumentException.class)
    public void rejectsInterspersedEmptyComponents() {
      MyPath.ofComponents("Test1", "", "Test2");
    }
    @Test(expected = IllegalArgumentException.class)
    public void rejectsSingleEmptyComponent() {
      MyPath.ofComponents("");
    }
    @Test
    public void returnsNotNullValue() {
      assertThat(MyPath.ofComponents("Test"), is(notNullValue()));
    }
  }
  public static class Root {
    @Test
    public void hasComponents() {
      assertThat(MyPath.ROOT.getComponents(), is(not(empty())));
    }
    @Test
    public void hasExactlyOneComponent() {
      assertThat(MyPath.ROOT.getComponents(), hasSize(1));
    }
    @Test
    public void hasExactlyOneInboxComponent() {
      assertThat(MyPath.ROOT.getComponents(), contains("ROOT"));
    }
    @Test
    public void isNotNull() {
      assertThat(MyPath.ROOT, is(notNullValue()));
    }
    @Test
    public void toStringIsSlashSeparatedAbsolutePathToInbox() {
      assertThat(MyPath.ROOT.toString(), is(equalTo("/ROOT")));
    }
  }
  public static class ToString {
    @Test
    public void toStringIsSlashSeparatedPathOfComponents() {
      String[] components = { "Test1", "Test2", "Test3" };
      String expectedPath =
          MyPath.SEPARATOR + Joiner.on(MyPath.SEPARATOR).join(components);
      assertThat(MyPath.ofComponents(components).toString(),
          is(equalTo(expectedPath)));
    }
  }
  @Test
  public void testPathCreationFromComponents() {
    String[] pathComponentArguments = new String[] { "One", "Two", "Three" };
    MyPath myPath = MyPath.ofComponents(pathComponentArguments);
    assertThat(myPath.getComponents(), contains(pathComponentArguments));
  }
}

明確に表現された質問

  • 単体テストを作成するために使用するテクニックのリストはありますか? 上記の非常に単純化されたリストよりもはるかに高度なもの (null のチェック、境界のチェック、予想される例外のチェックなど) は、購入する本やアクセスする URL で利用できるでしょうか?

  • 特定のタイプのパラメーターを取るメソッドを作成したら、 Eclipseプラグインを入手してテスト用のスタブを生成できますか? おそらく、Javaアノテーションを使用してメソッドに関するメタデータを指定し、ツールに関連するチェックを実体化させるのでしょうか? (例: @MustBeLowerCase、@ShouldBeOfSize(n=3)、...)

これらの「QA トリック」をすべて覚えたり、適用したりする必要があるのは、退屈でロボットのようだと思います。その上。確かに、Hamcrestライブラリはテストの種類を特殊化するという一般的な方向に進んでいますが (たとえば、RegEx を使用するStringオブジェクト、 Fileオブジェクトなど)、明らかにテスト スタブを自動生成せず、コードとそのプロパティを反映して準備しません。私のためのハーネス。

これを改善するのを手伝ってください。

PS

静的ファクトリメソッドで提供されたパスステップのリストからパスを作成するという概念の愚かなラッパーであるコードを提示しているだけだとは言わないでください。これは完全に作成された例ですが、「いくつかの" 引数の検証のケース...もっと長い例を含めると、誰がこの投稿を本当に読むでしょうか?

4

4 に答える 4

9
  1. の代わりにExpectedExceptionを使用することを検討してください@Test(expected...。これは、たとえば、を期待しNullPointerException、テストがセットアップでこの例外をスローした場合 (テスト対象のメソッドを呼び出す前に)、テストが成功するためです。ExpectedExceptionテスト中のメソッドへの呼び出しの直前に期待を置くと、この可能性はありません。また、ExpectedException例外メッセージをテストできます。これは、IllegalArgumentExceptionsスローされる可能性のある 2 つの異なるメッセージがあり、正しいメッセージを確認する必要がある場合に役立ちます。

  2. テスト中のメソッドをセットアップと検証から分離することを検討してください。これにより、テストのレビューとメンテナンスが容易になります。これは、テスト対象のクラスのメソッドがセットアップの一部として呼び出され、どちらがテスト対象のメソッドであるかを混乱させる可能性がある場合に特に当てはまります。次の形式を使用します。

    public void test() {
       //setup
       ...
    
       // test (usually only one line of code in this block)
       ...
    
       //verify
       ...
    }
    
  3. 参考になる本: Clean CodeJUnit In ActionTest Driven Development By Example

    Clean Codeには、テストに関する優れたセクションがあります

  4. 私が見たほとんどの例 (Eclipse が自動生成するものを含む) では、テストのタイトルにテスト中のメソッドが含まれています。これにより、レビューとメンテナンスが容易になります。例: testOfComponents_nullCase. あなたの例は、 を使用してEnclosedテスト中のメソッドごとにメソッドをグループ化する最初の例です。これは本当に素晴らしいことです。ただし、一部のオーバーヘッドが追加され、同封されたテスト クラス間で共有さ@Before@Afterません。

  5. まだ使い始めていませんが、Guava にはテスト ライブラリguava-testlibがあります。私はそれで遊ぶ機会がありませんでしたが、いくつかのクールなものがあるようです. 例: NullPointerTestは引用です:

  • * いずれかのパラメーターが null の場合に、メソッドが {@link * NullPointerException} または {@link UnsupportedOperationException} をスローすることを検証するテスト ユーティリティ。これを使用するには、まず、クラスで使用されるパラメーターの型に有効な * デフォルト値を指定する必要があります。

レビュー: 上記のテストは単なる例であると認識していますが、建設的なレビューが役立つ可能性があるため、どうぞ。

  1. testinggetComponentsでは、空のリストのケースもテストします。また、使用しますIsIterableContainingInOrder

  2. のテストでは、 を呼び出したり、さまざまな非エラー ケースを適切に処理したことを検証したりofComponentsすることは理にかなっているようです。に引数が渡されないテストが必要です。これはで行われているようですが、なぜそうしないのですか? 渡された値の 1 つであるテストが必要です。これは NPE をスローするためです。getComponentstoStringofComponentsofComponents( new String[]{})ofComponents()nullofComponents("blah", null, "blah2")

  3. テストROOTでは、以前に指摘したように、ROOT.getComponents一度呼び出して、3 つの検証すべてを行うことをお勧めします。また、ItIterableContainingInOrdernot empty、size、contains の 3 つすべてを行います。テストのisは無関係であり(言語学的ですが)、持つ価値がないと感じています(IMHO)。

  4. テストtoStringでは、テスト対象のメソッドを分離することが非常に役立つと感じています。toStringIsSlashSeparatedPathOfComponents以下のように書けばよかった。テスト対象のクラスの定数を使用していないことに注意してください。これは、私見によると、テスト対象のクラスに機能的な変更を加えると、テストが失敗するためです。

    @Test     
    public void toStringIsSlashSeparatedPathOfComponents() {       
       //setup 
       String[] components = { "Test1", "Test2", "Test3" };       
       String expectedPath =  "/" + Joiner.on("/").join(components);   
       MyPath path = MyPath.ofComponents(components)
    
       // test
       String value = path.toStrign();
    
       // verify
       assertThat(value, equalTo(expectedPath));   
    } 
    
  5. Enclosed内部クラスにない単体テストは実行されません。したがってtestPathCreationFromComponents、実行されません。

最後に、テスト駆動開発を使用します。これにより、テストが正しい理由で合格し、期待どおりに失敗することが保証されます。

于 2012-09-20T13:33:50.603 に答える
3

クラスを実際にテストするために多大な努力を払っていることがわかります。良い!:)

私のコメント/質問は次のとおりです。

  • 嘲笑はどうですか?あなたはこれのためのツールについて言及していません
  • テストされたクラスのビジネス上の目的を無視しながら、あなたは核心的な詳細に気を配っているように思えます (重要ではないとは言いません!)。それは、あなたがコードを最初にコーディングしているという事実から来ていると思います (そうですか?)。私が提案するのは、より多くの TDD/BDD アプローチと、テストされたクラスのビジネス責任に焦点を当てることです。
  • これが何をもたらすかわからない:「静的ネストされたクラスにグループ化されたメソッドテスト」?
  • テストスタブなどの自動生成に関して。簡単に言えば、しないでください。動作ではなく、実装をテストすることになります。
于 2012-09-20T19:02:10.133 に答える
2

わかりました、これがあなたの質問に対する私の見解です:

単体テストを作成するために使用するテクニックのリストはありますか?

短い答え、いいえ。あなたの問題は、メソッドのテストを生成するには、メソッドが何をするかを分析し、各場所で可能な値ごとにテストを行う必要があることです。テスト ジェネレーターはありましたが、IIRC では保守可能なコードを生成しませんでした (「テスト駆動開発のリソース」を参照)。

チェックすべき項目のかなり良いリストがすでにあります。これに追加します。

  • メソッドを通るすべてのパスがカバーされていることを確認してください。
  • すべての重要な機能が複数のテストでカバーされていることを確認してください。私はこのためにパラメータ化を多用しています。

私が本当に便利だと思うことの 1 つは、このメソッドが何をするかではなく、このメソッドが何をすべきかを尋ねることです。このようにして、よりオープンな心でテストを記述します。

もう 1 つの便利な点は、テストに関連するボイラープレートを削減することです。これにより、テストをより簡単に読み取ることができます。テストを追加するのが簡単であればあるほど良いです。これには Parameterized が非常に適していると思います。私にとって、テストの読みやすさは重要です。

したがって、上記の例を取り上げると、「メソッド内の 1 つだけをテストする」という要件を削除すると、次のようになります。

public static class Root {
  @Test
  public void testROOT() {
    assertThat("hasComponents", MyPath.ROOT.getComponents(), is(not(empty())));
    assertThat("hasExactlyOneComponent", MyPath.ROOT.getComponents(), hasSize(1));
    assertThat("hasExactlyOneInboxComponent", MyPath.ROOT.getComponents(), contains("ROOT"));
    assertThat("isNotNull", MyPath.ROOT, is(notNullValue()));
    assertThat("toStringIsSlashSeparatedAbsolutePathToInbox", MyPath.ROOT.toString(), is(equalTo("/ROOT")));
  }
}

説明をアサートに追加し、すべてのテストを 1 つにマージしました。これで、テストを読み取って、実際に重複したテストがあることを確認できます。is(not(empty())おそらく&&などをテストする必要はありませんis(notNullValue())。これは、メソッドごとに 1 つのアサートのルールに違反していますが、カバレッジを減らすことなく多くのボイラープレートを削除したため、正当化されると思います。

チェックを自動的に実行できますか?

はい。しかし、私はそれを行うために注釈を使用しません。次のようなメソッドがあるとします。

public boolean validate(Foobar foobar) {
  return !foobar.getBar().length > 40;
} 

だから私は次のようなことを言うテストメソッドを持っています:

private Foobar getFoobar(int length) {
  Foobar foobar = new Foobar();
  foobar.setBar(StringUtils.rightPad("", length, "x")); // make string of length characters
  return foobar;
}

@Test
public void testFoobar() {
  assertEquals(true, getFoobar(39));
  assertEquals(true, getFoobar(40));
  assertEquals(false, getFoobar(41));
}

上記の方法は、長さに応じて、もちろんパラメーター化されたテストに分解するのに十分簡単です。話の教訓として、テスト以外のコードと同じように、テストを因数分解できます。

あなたの質問に答えるために、私の経験では、パラメーター化と因数分解の賢明な組み合わせを使用して、テスト内のボイラープレートを削減することにより、すべての組み合わせを支援するために多くのことができるという結論に達しましたテスト。最後の例として、これは私がパラメータ化されたテストをどのように実装するかです:

@RunWith(Parameterized.class) public static class OfComponents { @Parameters public static Collection data() { return Arrays.asList(new Object[][] { { new String[] {"Test1", "Test2", "Test3" }, null }, { new String[] {"Test1"}, null }, { null, NullPointerException.class }, { new String[] {"Test1", "", "Test2"}, IllegalArgumentException }, }) ; }

private String[] components;

@Rule
public TestRule expectedExceptionRule = ExpectedException.none();

public OfComponents(String[] components, Exception expectedException) {
   this.components = components;
   if (expectedException != null) {
     expectedExceptionRule.expect(expectedException);
   }
}

@Test
public void test() {
  MyPath.ofComponents(components);
}

上記はテストされておらず、おそらくコンパイルできないことに注意してください。上記から、データを入力として分析し、すべての組み合わせをすべて追加 (または少なくとも追加を検討) できます。たとえば、{"Test1", null, "Test2"} のテストがありません ...

于 2012-09-20T13:50:19.097 に答える
0

さて、私は2つの違いの答えを投稿します。

  1. James Coplien が述べたように、単体テストは無価値です。私はこの問題について彼に同意しませんが、自動解決策を探すのではなく、単体テストを減らすことを検討するのに役立つかもしれません.

  2. DataPoints で Theories を使用することを検討してください。これにより、問題が大幅に軽減されると思います。また、モックを使用すると役立ちます。

于 2012-09-23T17:48:04.643 に答える