web-dev-qa-db-ja.com

JUnitテストですべての変数を宣言することの利点/欠点

作業中の新しいコードの単体テストをいくつか作成していて、コードレビューのために送りました。同僚の1人が、多くのテストで使用される変数をテストのスコープ外に置いた理由についてコメントしました。

私が投稿したコードは本質的に

_import org.junit.Test;

public class FooUnitTests {

    @Test
    public void testConstructorWithValidName() {
        new Foo(VALID_NAME);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithNullName() {
        new Foo(null);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithZeroLengthName() {
        new Foo("");
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithLeadingAndTrailingWhitespaceInName() {
        final String name = " " + VALID_NAME + " ";
        final Foo foo = new Foo(name);

        assertThat(foo.getName(), is(equalTo(VALID_NAME)));
    }

    private static final String VALID_NAME = "name";
}
_

彼の提案された変更は本質的に

_import org.junit.Test;

public class FooUnitTests {

    @Test
    public void testConstructorWithValidName() {
        final String name = "name";
        final Foo foo = new Foo(name);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithNullName() {
        final String name = null;
        final Foo foo = new Foo(name);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithZeroLengthName() {
        final String name = "";
        final Foo foo = new Foo(name);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithLeadingAndTrailingWhitespaceInName() {
        final String name = " name ";
        final Foo foo = new Foo(name);

        final String actual = foo.getName();
        final String expected = "name";

        assertThat(actual, is(equalTo(expected)));
    }
}
_

テストの範囲内で必要なeverythingは、テストの範囲内で定義されます。

彼が主張したいくつかの利点は

  1. 各テストは自己完結型です。
  2. 各テストは単独で、または集約して実行でき、同じ結果になります。
  3. レビューアーは、これらのパラメーターが宣言されている場所にスクロールして、値を調べる必要はありません。

私が主張した彼の方法の欠点のいくつかは

  1. コードの重複を増やす
  2. 異なる値が定義されている同様のテストが複数ある場合(つまり、doFoo(bar)を使用したテストは1つの値を返しますが、barはその方法で異なる定義)。

慣例以外に、どちらか一方の方法を使用することの他の利点/欠点はありますか?

9
Zymus

自分がやっていることを続けてください。

自分自身を繰り返すのは、ビジネスコードと同じようにテストコードでも同じように悪い考えです。すべてのテストは自己完結型である必要があるという考えに同僚は惑わされました。それは事実ですが、「自己完結型」は、メソッド本体内に必要なものすべてが含まれている必要があるという意味ではありません。これは、それがisolatonで実行されても、スイートの一部として実行されても、スイート内のテストの順序に関係なく、同じ結果が得られることを意味します。言い換えると、テストコードは、他のコードの前に実行されたものに関係なく、同じセマンティクスを持つ必要があります。必要なすべてのコードをテキストでバンドルする必要はありません

定数やセットアップコードなどを再利用すると、テストコードの品質が向上し、自己完結性が損なわれることがないため、継続して実行することをお勧めします。

10
Kilian Foth

場合によります。一般に、繰り返し定数を1か所だけで宣言するのは良いことです。

ただし、この例では、メンバーVALID_NAMEを示されている方法で定義しても意味がありません。 2番目のバリアントで、メンテナが最初のテストでテキストnameを変更すると仮定すると、2番目のテストはおそらくまだ有効であり、逆もまた同様です。たとえば、大文字と小文字をさらにテストしたいが、できるだけ少ないテストケースで小文字だけのテストケースも保持するとします。新しいテストを追加する代わりに、最初のテストを

public void testConstructorWithValidName() {
    final String name = "Name";
    final Foo foo = new Foo(name);
}

残りのテストを維持します。

実際、そのような変数に応じて多くのテストを行い、後でVALID_NAMEの値を変更すると、後で意図せずに中断するテストの一部が実行される場合があります。そして、そのような状況では、それに応じて異なるテストで人工定数を導入することで、テストの自己完結性を実際に改善できますnot

ただし、@ KilianFothが自分自身を繰り返さないように書いたことも正しいです:テストコードのDRY原則は、ビジネスコードで行うのと同じ方法に従ってください。たとえば、定数またはメンバー変数をテストコードに不可欠それらの値がすべての場所で同じであること。

あなたの例は、テストが初期化コードを繰り返す傾向があることを示しています。これは、リファクタリングから始める典型的な場所です。だからあなたはの繰り返しをリファクタリングすることを検討するかもしれません

  new Foo(...);

別の関数に

 public Foo ConstructFoo(String name) 
 {
     return new Foo(name);
 }

Fooコンストラクターのシグネチャを後で簡単に変更できるため(new Fooが実際に呼び出される場所が少ないため)。

3
Doc Brown

私は実際に...に行きませんboth、しかし私はあなたのcoよりあなたのものを選びます-あなたの簡単な例のための労働者。

まず最初に、1回限りの割り当てではなく値のインライン化を優先する傾向があります。これにより、一連の思考を複数の割り当てステートメントに分割する必要がないので、コードの読みやすさが向上します。その後、それらが使用されているコード行を読み取ります。したがって、testConstructorWithLeadingAndTrailingWhitespaceInName()がおそらくちょうど次のようになるわずかなヒントを使用して、同僚よりもあなたのアプローチを優先します。

assertThat(new Foo(" " + VALID_NAME + " ").getName(), is(equalTo(VALID_NAME)));

それは私に命名をもたらします。通常、テストがExceptionをスローするようにアサートしている場合は、わかりやすくするために、テスト名にその動作も記述してください。上記の例を使用して、Fooオブジェクトを作成するときに名前に余分な空白がある場合はどうなりますか?そして、それはIllegalArgumentExceptionをスローすることとどのように関連していますか?

nullおよび""テストに基づいて、今回はgetName()を呼び出すために同じExceptionがスローされたと想定しています。そうだとすれば、テストメソッドの名前をcallingGetNameWithWhitespacePaddingThrows()IllegalArgumentException-これはJUnitの出力の一部になると考えられるため、オプションである)に設定する方がよいでしょう。インスタンス化中に名前に空白のパディングがあると、同じExceptionもスローされる場合は、アサーションも完全にスキップして、コンストラクターの動作にあることを明確にします。

ただし、有効な入力と無効な入力の順列を増やす場合、つまり パラメータ化されたテスト の場合に、より良い方法があると思います。あなたがテストしているのはまさにそれです:異なるコンストラクタ引数でFooオブジェクトの束を作成し、それをインスタンス化で失敗するか、またはgetName()を呼び出す 。したがって、JUnitに反復とスキャフォールディングを実行させてみませんか?簡単に言えば、JUnitに「これらはFooオブジェクトを作成するときに使用する値です」と伝え、適切な場所でIllegalArgumentExceptionに対してテストメソッドをアサートすることができます。残念ながら、JUnitのパラメーター化されたテストの方法は、同じテストで成功した場合と例外的な場合の両方のテストではうまく機能しないため(おそらく私が認識している限り)、おそらく次の構造:

@RunWith(Parameterized.class)
public class FooUnitTests {

    private static final String VALID_NAME = "ABC";

    @Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] {
                 { VALID_NAME, false }, 
                 { "", true }, 
                 { null, true }, 
                 { " " + VALID_NAME + " ", true }
           });
    }

    private String input;

    private boolean throwException;

    public FooUnitTests(String input, boolean throwException) {
        this.input = input;
        this.throwException = throwException;
    }

    @Test
    public void test() {
        try {
            Foo test = new Foo(input);
            // TODO any testing required for VALID_NAME?
            if (throwException) {
                assertEquals(VALID_NAME, test.getName());
            }
        } catch (IllegalArgumentException e) {
            if (!throwException) {
                throw new RuntimeException(e);
            }
        }
    }
}

確かに、これは、他の点では単純な4値テストが何であるかについては不格好に見え、JUnitのやや制限のある実装ではあまり役に立たない。この点に関して、私は TestNGの@DataProvider アプローチがより有用であることを発見しました。パラメータ化されたテストを検討している場合は、詳細に検討する価値があります。

ユニットテストにTestNGを使用する方法は次のとおりです(Java 8's Stream を使用してIterator構成を簡略化する) 。いくつかの利点がありますが、これらに限定されません。

  1. テストパラメータは、クラスフィールドではなくメソッド引数になります。
  2. 異なる種類の入力を提供する複数の@DataProviderを使用できます。
    • テスト用に新しい有効/無効入力を追加するほうが間違いなく明確で簡単です。
  3. 単純化した例ではまだ少し見過ごされているように見えますが、私見ではJUnitのアプローチよりもきれいです。
public class FooUnitTests {

    private static final String VALID_NAME = "ABC";

    @DataProvider(name="successes")
    public static Iterator<Object[]> getSuccessCases() {
        return Stream.of(VALID_NAME).map(v -> new Object[]{ v }).iterator();
    }

    @DataProvider(name="exceptions")
    public static Iterator<Object[]> getExceptionCases() {
        return Stream.of("", null, " " + VALID_NAME + " ")
                    .map(v -> new Object[]{ v }).iterator();
    }

    @Test(dataProvider="successes")
    public void testSuccesses(String input) {
        new Foo(input);
        // TODO any further testing required?
    }

    @Test(dataProvider="exceptions", expectedExceptions=IllegalArgumentException.class)
    public void testExceptions(String input) {
        Foo test = new Foo(input);
        if (input.contains(VALID_NAME)) {
            // TestNG favors assert(actual, expected).
            assertEquals(test.getName(), VALID_NAME);
        }
    }
}
1
h.j.k.