web-dev-qa-db-ja.com

1つの単体テストで複数のアサートを使用しても問題ありませんか?

this great post へのコメントで、Roy Osheroveは、単一のテストで各アサートを実行するように設計された [〜#〜] oapt [〜#〜] プロジェクトについて言及しました。

以下はプロジェクトのホームページに書かれています:

適切な単体テストは1つの理由で失敗するはずです。そのため、単体テストごとに1つのアサートを使用する必要があります。

そして、また、ロイはコメントで書いています:

私のガイドラインは通常、テストごとに1つの論理CONCEPTをテストすることです。同じobjectに複数のアサートを設定できます。彼らは通常、テストされている同じ概念になります。

複数のアサーションが必要な場合もあると思います(例 Guard Assertion )が、一般的にはこれを回避しようとします。あなたの意見は何ですか?複数のアサートが本当に必要な実際の例を提供してください

430
Restuta

それは必ずしも悪いことだとは思わないが、私たちは単一のアサートのみを持つように努力すべきだと思う私たちのテストでは。これは、あなたがより多くのテストを書くことを意味し、私たちのテストは一度に一つだけのものをテストすることになります。

そうは言っても、私のテストの半分は実際には1つのアサートしか持っていないと言えるでしょう。テストに約5つ以上のアサートがある場合にのみ、コード(テスト?)においになると思います。

複数のアサートをどのように解決しますか?

246
Jaco Pretorius

テストは1つの理由でのみ失敗するはずですが、それは必ずしもAssertステートメントが1つだけであることを意味するわけではありません。私見では、「 アレンジ、アクト、アサート 」のパターンを維持することがより重要です。

重要なのは、アクションが1つだけあり、アサートを使用してそのアクションの結果を検査することです。しかし、それは「アレンジ、アクト、アサート、テストの終了」です。別のアクションを実行してテストを続行し、その後さらにアサートしたい場合は、代わりに別のテストを作成してください。

同じアクションのテストの一部を形成する複数のassertステートメントを見てうれしいです。例えば.

[Test]
public void ValueIsInRange()
{
  int value = GetValueToTest();

  Assert.That(value, Is.GreaterThan(10), "value is too small");
  Assert.That(value, Is.LessThan(100), "value is too large");
} 

または

[Test]
public void ListContainsOneValue()
{
  var list = GetListOf(1);

  Assert.That(list, Is.Not.Null, "List is null");
  Assert.That(list.Count, Is.EqualTo(1), "Should have one item in list");
  Assert.That(list[0], Is.Not.Null, "Item is null");
} 

あなたはcouldこれらを1つのアサートに結合しますが、それはshouldまたはmustと主張することとは異なります。それらを組み合わせても改善はありません。

例えば最初のものcould be

Assert.IsTrue((10 < value) && (value < 100), "Value out of range"); 

しかし、これは良くありません-そこからのエラーメッセージはそれほど具体的ではなく、他の利点はありません。 2つまたは3つ(またはそれ以上)のアサートを1つの大きなブール条件に組み合わせると、読み取りが困難になり、変更が困難になり、失敗した理由を解明するのが困難になる他の例を思いつくと思います。ルールのためだけにこれを行うのはなぜですか?

[〜#〜] nb [〜#〜]:ここで書いているコードはC#とNUnitですが、原則は他の言語でも保持されますとフレームワーク。構文も非常に似ている場合があります。

314
Anthony

複数のアサートが悪いことだとは思っていません。

私はいつもそれをしています:

public void ToPredicateTest()
{
    ResultField rf = new ResultField(ResultFieldType.Measurement, "name", 100);
    Predicate<ResultField> p = (new ConditionBuilder()).LessThanConst(400)
                                                       .Or()
                                                       .OpenParenthesis()
                                                       .GreaterThanConst(500)
                                                       .And()
                                                       .LessThanConst(1000)
                                                       .And().Not()
                                                       .EqualsConst(666)
                                                       .CloseParenthesis()
                                                       .ToPredicate();
    Assert.IsTrue(p(ResultField.FillResult(rf, 399)));
    Assert.IsTrue(p(ResultField.FillResult(rf, 567)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 400)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 666)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 1001)));

    Predicate<ResultField> p2 = (new ConditionBuilder()).EqualsConst(true).ToPredicate();

    Assert.IsTrue(p2(new ResultField(ResultFieldType.Confirmation, "Is True", true)));
    Assert.IsFalse(p2(new ResultField(ResultFieldType.Confirmation, "Is False", false)));
}

ここでは、複数のアサートを使用して、複雑な条件を期待される述語に変換できることを確認しています。

私は1つのユニット(ToPredicateメソッド)のみをテストしていますが、テストで考えられるすべてのものをカバーしています。

85
Matt Ellen

ユニットテストを使用して高レベルの動作を検証する場合、複数のアサーションを1つのテストに完全に入れます。これが私がいくつかの緊急通知コードに実際に使用しているテストです。テストの前に実行されるコードにより、システムは、メインプロセッサが実行されるとアラームが送信される状態になります。

@Test
public void testAlarmSent() {
    assertAllUnitsAvailable();
    assertNewAlarmMessages(0);

    pulseMainProcessor();

    assertAllUnitsAlerting();
    assertAllNotificationsSent();
    assertAllNotificationsUnclosed();
    assertNewAlarmMessages(1);
}

これは、コードが期待どおりの動作をしていることを確信できるようにするために、プロセスのすべてのステップで存在する必要がある条件を表しています。単一のアサーションが失敗した場合でも、残りのアサーションが実行されなくてもかまいません。システムの状態が無効になっているため、それらの後続のアサーションは価値のあるものを教えてくれません。* assertAllUnitsAlerting()が失敗した場合、assertAllNotificationSent()の対処方法がわかりません。成功OR失敗し、以前のエラーの原因を特定して修正しました。

(*-さて、それらは問題のデバッグにおそらく役立つかもしれません。しかし、テストが失敗したという最も重要な情報はすでに受け取られています。)

21
BlairHippo

1つのメソッド内の複数のアサートが悪いことではないと私が思うもう1つの理由は、次のコードで説明されています。

_class Service {
    Result process();
}

class Result {
    Inner inner;
}

class Inner {
    int number;
}
_

私のテストでは、service.process()Innerクラスインスタンスで正しい数を返すことをテストしたいだけです。

テストする代わりに...

_@Test
public void test() {
    Result res = service.process();
    if ( res != null && res.getInner() != null ) Assert.assertEquals( ..., res.getInner() );
}
_

私がやっている

_@Test
public void test() {
    Result res = service.process();
    Assert.notNull(res);
    Assert.notNull(res.getInner());
    Assert.assertEquals( ..., res.getInner() );
}
_
8
Betlista

テストが1つの理由でのみ失敗するというルール内で、複数のアサートの記述が有効であるケースはたくさんあると思います。

たとえば、日付文字列を解析する関数を想像してください。

function testParseValidDateYMD() {
    var date = Date.parse("2016-01-02");

    Assert.That(date.Year).Equals(2016);
    Assert.That(date.Month).Equals(1);
    Assert.That(date.Day).Equals(0);
}

テストが失敗した場合、1つの理由が原因で、解析は正しくありません。このテストが3つの異なる理由で失敗する可能性があると主張する場合、「1つの理由」の定義で私見が細かくなりすぎるでしょう。

6
Pete

[Test]メソッド自体の内部に複数のアサーションを作成するのが良いと思われる状況は知りません。人々が複数のアサーションを持つことを好む主な理由は、テストされる各クラスに対して1つの[TestFixture]クラスを用意しようとしているためです。代わりに、テストをより多くの[TestFixture]クラスに分割できます。これにより、最初のアサーションが失敗した方法だけでなく、コードが予期したとおりに反応しなかった可能性がある複数の方法を確認できます。これを達成する方法は、多くの[TestFixture]クラスが内部でテストされているクラスごとに少なくとも1つのディレクトリがあることです。各[TestFixture]クラスは、テストするオブジェクトの特定の状態に基づいて名前が付けられます。 [SetUp]メソッドは、オブジェクトをクラス名で記述された状態にします。次に、オブジェクトの現在の状態を前提として、それぞれがtrueであると期待するさまざまなことを表明する複数の[Test]メソッドがあります。各[Test]メソッドの名前は、アサートしているものにちなんで付けられています。ただし、英語のコードの読みではなく、概念に基づいて名前が付けられている場合があります。その後、各[Test]メソッドの実装は、何かをアサートする1​​行のコードのみを必要とします。このアプローチのもう1つの利点は、クラスとメソッドの名前を見ただけで、何をテストし、何を期待しているのかが非常に明確になるため、テストが非常に読みやすくなることです。これは、テストしたいすべての小さなEdgeケースを認識し始め、バグを発見するにつれて、より適切にスケーリングされます。

通常、これは、[SetUp]メソッド内の最後のコード行が[TestFixture]のプライベートインスタンス変数にプロパティ値または戻り値を格納する必要があることを意味します。次に、異なる[Test]メソッドから、このインスタンス変数について複数の異なるものをアサートする場合があります。また、テスト対象のオブジェクトが目的の状態になっているため、オブジェクトのさまざまなプロパティがどのように設定されているかについてもアサーションを作成できます。

オブジェクトを目的の状態にする前にめちゃくちゃになっていないことを確認するために、テスト中のオブジェクトを目的の状態にするときに、途中でアサーションを作成する必要がある場合があります。その場合、これらの追加のアサーションは[SetUp]メソッド内に表示されます。 [SetUp]メソッド内で問題が発生した場合は、オブジェクトがテストしたい状態になる前に、テストに問題があったことが明らかになります。

発生する可能性のあるもう1つの問題は、スローされると予期していた例外をテストしている可能性があります。これは、上記のモデルに従わないように誘惑する可能性があります。ただし、[SetUp]メソッド内の例外をキャッチしてインスタンス変数に格納することで、引き続き達成できます。これにより、例外についてさまざまなことをアサートできます。それぞれ独自の[Test]メソッド内にあります。次に、テスト対象のオブジェクトについて他のことをアサートして、スローされた例外による意図しない副作用がないことを確認することもできます。

例(これは複数のファイルに分割されます):

namespace Tests.AcctTests
{
    [TestFixture]
    public class no_events
    {
        private Acct _acct;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
        }

        [Test]
        public void balance_0() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_0
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(0m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_negative
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(-0.01m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }
}
3

1つのテスト関数に複数のアサーションがある場合、それらは、実行しているテストに直接関連していると思います。例えば、

@Test
test_Is_Date_segments_correct {

   // It is okay if you have multiple asserts checking dd, mm, yyyy, hh, mm, ss, etc. 
   // But you would not have any assert statement checking if it is string or number,
   // that is a different test and may be with multiple or single assert statement.
}

多くのテストを行うことは(おそらくそれが多すぎると感じる場合でも)、悪いことではありません。重要で最も重要なテストを行うことがより重要であると主張することができます。 SO、アサートするときは、複数のアサートを心配しすぎないで、アサートステートメントが正しく配置されていることを確認してください。複数必要な場合は、複数使用してください。

2
hagubear

同じテストに複数のアサーションがあることは、テストが失敗したときにのみ問題になります。次に、テストをデバッグするか、例外を分析して、失敗したアサーションを特定する必要があります。各テストで1つのアサーションを使用すると、通常、何が問題なのかを簡単に特定できます。

複数のアサーションが実際に必要であるシナリオは、同じアサーションの複数の条件としていつでも書き直すことができるため、考えられません。ただし、たとえば入力の誤りが原因で後のステップがクラッシュする危険を冒すのではなく、ステップ間の中間データを検証するためのいくつかのステップがある場合は、望ましい場合があります。

2
Guffa

テストが失敗した場合、次のアサーションが失敗するかどうかもわかりません。多くの場合、これは問題の原因を解明するための貴重な情報が欠落していることを意味します。私の解決策は、1つのアサートを使用することですが、いくつかの値があります。

String actual = "val1="+val1+"\nval2="+val2;
assertEquals(
    "val1=5\n" +
    "val2=hello"
    , actual
);

これにより、失敗したすべてのアサーションを一度に確認できます。ほとんどのIDEは比較ダイアログに文字列の違いを並べて表示するため、数行を使用しています。

2
Aaron Digulla

単体テストの目的は、何が失敗しているかについてできるだけ多くの情報を提供することですが、最も基本的な問題を最初に正確に特定するのにも役立ちます。別のアサーションが失敗した場合、つまりテスト間に依存関係がある場合に、1つのアサーションが失敗することを論理的に理解している場合、これらを単一のテスト内で複数のアサートとしてロールすることは理にかなっています。これには、単一のテスト内の最初のアサーションで救済された場合に排除できたであろう明らかな失敗がテスト結果に散らばらないという利点があります。この関係が存在しない場合、これらのアサーションを個別のテストに分離することが自然に優先されます。それ以外の場合、これらの失敗を見つけるには、すべての問題を解決するためにテスト実行を複数回繰り返す必要があるためです。

次に、過度に複雑なテストを作成する必要があるようにユニット/クラスを設計すると、テスト中の負担が減り、おそらくより良い設計が促進されます。

1
jpierson

はい、複数のアサーションがあっても問題ありません限り失敗したテストは、失敗を診断できる十分な情報を提供します。これは、何をテストしているか、どの障害モードが何であるかによって異なります。

適切なユニットテストは、1つの理由で失敗するはずです。そのため、ユニットテストごとに1つのアサートを使用する必要があります。

私はそのような定式化が役立つとは思いませんでした(クラスを変更する理由が1つあるべきであるというのは、そのような役に立たない格言の例です)。 2つの文字列が等しいというアサーションを考えます。これは、2つの文字列の長さが同じで、対応するインデックスの各文字が等しいというアサーションと意味的に同等です。

一般化して、複数のアサーションのシステムは単一のアサーションとして書き直すことができ、単一のアサーションは小さなアサーションのセットに分解できると言えます。

したがって、コードの明快さとテスト結果の明快さに焦点を当て、その逆ではなく、使用するアサーションの数をガイドするようにします。

1
CurtainDog

この質問は、スパゲッティとラザニアのコード問題のバランスをとるという古典的な問題に関連しています。

複数のアサートを使用すると、テストの内容がわからないスパゲッティ問題に簡単に陥る可能性がありますが、テストごとに1つのアサートを使用すると、大きなラザニアに複数のテストが存在するため、テストが同じように読めなくなり、どのテストが不可能かを見つけることができます。 。

いくつかの例外がありますが、この場合は振り子を真ん中に置くことが答えです。

0
gsf

答えは非常に簡単です。同じオブジェクト、または2つの異なるオブジェクトの複数の属性を変更する関数をテストし、関数の正確さがそれらのすべての変更の結果に依存している場合、アサートする必要があります。これらすべての変更が適切に実行されていることを確認してください。

私は論理的な概念のアイデアを得ましたが、逆の結論は、関数は決して複数のオブジェクトを変更してはならないということです。しかし、私の経験では、それをすべての場合に実装することは不可能です。

銀行取引の論理的な概念を取り上げます。ほとんどの場合、ある銀行口座から金額を引き出すには、その金額を別の口座に追加する必要があります。これらの2つを分離したくない場合は、アトミックユニットを形成します。 2つの関数(withdraw/addMoney)を作成して、2つの異なる単体テストを作成することもできます。ただし、これら2つのアクションは1つのトランザクション内で実行する必要があり、トランザクションが機能することを確認する必要もあります。その場合、個々の手順が成功したことを確認するだけでは不十分です。テストでは、両方の銀行口座を確認する必要があります。

そもそも単体テストではなく、統合テストや受け入れテストでテストするより複雑な例があるかもしれません。しかし、それらの境界は流暢です、私見!それは決定するのは簡単ではありません、それは状況の問題であり、おそらく個人的な好みです。 1つからお金を引き出して別のアカウントに追加することは、まだ非常に簡単な機能であり、間違いなく単体テストの候補です。

0
cslotty