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


9

作業中の新しいコードの単体テストをいくつか作成し、コードレビューのために送りました。私の同僚の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)));
    }
}

どこにすべての試験の範囲内で必要とされ、テストの範囲内で定義されています。

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

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

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

  1. コードの重複を増やす
  2. 異なる値が定義されている同様のテストが複数ある場合(つまりdoFoo(bar)、同じ呼び出しがbarそのメソッドで異なるように定義されているため、同じ呼び出しで異なる結果が得られる一方で、結果が1つの値である場合)、レビュー担当者の心にノイズを追加できます。

慣例は別として、どちらの方法を使用しても、他の方法に比べて他の利点/欠点はありますか?


コードで私が目にする唯一の問題は、VALID_NAMEの宣言を下部に埋め込んでいることです。「スクロール」を処理する修正。同僚のローカルスコープ名が理由なくDRYに違反しています。各テストメソッドの外部にあるインポートステートメントを使用してもよい理由を尋ねます。それはコンテキストと呼ばれています。
candied_orange 2015年

@CandiedOrange:誰もここで私の答えを読んでいないのですか?同僚のアプローチはDRYの原則に違反する可能性がありますが、この例ではそうではありません。これらすべてのケースで同じテキスト「名前」を使用しても意味がありません。
ドクターブラウン

追加の変数は明確さを追加しませんが、Junitの理論とデータポイントを使用するようにテストを書き直すことができることを指摘しています。
ローザリヒター2015年

1
@CandiedOrange:DRYは、知識(事実、ルールなど)の重複を回避することよりも、コードの重複を回避することの方が少ないです。参照:hermanradtke.com/2013/02/06/misunderstanding-dry.htmlドライの原則は、「すべての知識は、システム内で単一の明確な権威ある表現を持たなければならない」と述べられています。en.wikipedia.org/wiki/Don%27t_repeat_yourself
Marjan Venema

「すべての知識は、システム内で単一の明確で信頼できる表現を持つ必要があります。」では、「名前」がVALID_NAMEであるという事実は、これらの知識の1つではありませんか?DRYがコードの重複を回避するだけではないことは事実ですが、コードが重複してDRYになるのは難しいと感じるでしょう。
candied_orange

回答:


10

あなたは自分がやっていることを続けなければなりません。

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

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


絶対に+1しますが、DRYは繰り返しコードを回避することよりも、繰り返し知識を繰り返さないことよりも重要です。hermanradtke.com/2013/02/06/misunderstanding-dry.htmlを参照してください。ドライの原則は、「すべての知識は、システム内に単一の明確で信頼できる表現を持たなければならない」と述べられています。en.wikipedia.org/wiki/Don%27t_repeat_yourself
Marjan Venema

3

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

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

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

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

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

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

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

  new Foo(...);

別の関数に

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

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


これは非常に短いコード例です。量産コードでは、VALID_NAME変数が約30回使用されています。しかし、それは非常に食べ物のポイントであり、私はそうではありませんでした。
Zymus、

@Zymus:VALID_NAMEが使用される回数はIMHOとは無関係です-重要なのは、テストですべての場合に同じ「名前」テキストを使用することが重要である場合(そのために記号を導入することが理にかなっているため)、またはシンボルを導入すると、人為的で不要な依存関係が作成されます。私は2番目のタイプの私見であるあなたの例しか見ることができませんが、「実際の」コードの場合、走行距離は異なる場合があります。
Doc Brown、

1
あなたが作るケースのための+1。また、テストコードは製品グレードのコードである必要があります。とはいえ、DRYは自分自身をまったく繰り返さないということではありません。それは知識を繰り返さないことです。DRYの原則は、「すべての知識は、システム内に単一の明確で信頼できる表現を持たなければならない」と述べられています。(en.wikipedia.org/wiki/Don%27t_repeat_yourself)。誤解は、私が思うように「繰り返し」から発生します:hermanradtke.com/2013/02/06/misunderstanding-dry.html
Marjan Venema

1

私は実際には... 両方行くことはしませんが、簡単な例のためにあなたの同僚よりあなたのものを選びます。

まず最初に、1回限りの割り当てではなく値のインライン化を優先する傾向があります。私は、複数の代入文に思考の私の列車を破ることはありませんので、これは私のためのコードの可読性を向上させ、その後、それらが使用されているコードの行(複数可)をお読みください。したがって、同僚よりもあなたのアプローチを好むtestConstructorWithLeadingAndTrailingWhitespaceInName()でしょう。

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

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

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

ただし、有効な入力と無効な入力の順列を増やす場合、つまりパラメーター化されたテストを行う場合、ここでより良い方法があると思います。あなたがテストしているのはまさにそれです:Foo異なるコンストラクター引数でオブジェクトの束を作成し、それをインスタンス化またはの呼び出しで失敗することを表明していますgetName()。したがって、JUnitに反復とスキャフォールディングを実行させてみませんか?簡単に言えば、「これらはFooオブジェクトを作成したい値です」とJUnitに伝え、テストメソッドをアサートすることができます。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を使用しStreamIterator構成を簡略化します)。いくつかの利点がありますが、これらに限定されません。

  1. テストパラメータは、クラスフィールドではなくメソッド引数になります。
  2. @DataProvider異なる種類の入力を提供する複数のを使用できます。
    • テスト用に新しい有効/無効入力を追加するほうが間違いなく明確で簡単です。
  3. 単純化された例ではまだ少し誇張されているように見えますが、IMHOは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);
        }
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.