web-dev-qa-db-ja.com

単体テストでテストされているロジックを繰り返すことは悪い習慣ですか?

コードの単体テストを作成し、単体テストのロジックの一部または多くを繰り返して単体テストをDRYにすることがよくあります。

たとえば、次のコードを考えてみます。

function speak(conditionOne, conditionTwo) {
  if (conditionOne) {
    return "Hey"
  } else if (conditionTwo) {
    return "Sup"
  } else {
    return "Waddup"
  }
}

これで、たとえば、jestを使用してすべてのケースをハードコーディングすることにより、単体テストを作成できます。

describe('speak', () => {
  test('when condition one but not condition two', () => {
    expect(speak(true, false)).toEqual('Hey')
  })

  test('when condition one and condition two', () => {
    expect(speak(true, true)).toEqual('

  test('when not condition one but condition two', () => {
    expect(speak(false, true)).toEqual('Sup')
  })

  test('when not condition one and not condition two', () => {
    expect(speak(false, false)).toEqual('Waddup')
  })
})

または、構文シュガーを使用して少し簡潔にすることもできますが、ケースと予想される結果をハードコーディングします。

describe('speak', () => {
  test.each`
    conditionOne | conditionTwo | value
    ${true}      | ${false}     | ${'Hey'}
    ${true}      | ${true}      | ${'Hey'}
    ${false}     | ${true}      | ${'Sup'}
    ${false}     | ${false}     | ${'Waddup'}
  `('when conditionOne is $conditionOne and conditionTwo is $conditionTwo then $value', ({ conditionOne, conditionTwo, value }) => {
    expect(speak(conditionOne, conditionTwo)).toEqual(value)
  })
})

しかし、ロジックが適切な量である場合は、テーブルにハードコードするケースの数が指数関数的になり、結果を値としてハードコードすることが必ずしも容易ではないことを想像できます。

これは、テストをよりインテリジェントにすることをお勧めしますが、ユニットロジックの繰り返しになります(ユニットロジックが間違っているとどうなりますか?)

例えば:

describe('speak', () => {
  describe.each`
    conditionOne
    ${true}
    ${false}
  `('when conditionOne is $conditionOne', ({ conditionOne }) => {
     test.each`
       conditionTwo
       ${true}
       ${false}
     `('when conditionTwo is $conditionTwo', ({ conditionTwo }) => {
       let value
       if (conditionOne) {
         value = 'Hey'
       } else if (conditionTwo) {
         value = 'Sup'
       } else {
         value = 'Waddup'
       }

       expect(speak(conditionOne, conditionTwo)).toEqual(value)
     })
})

これにより、テーブルを手動で書き出すのが面倒な場合のテストがはるかに簡単になりました。ただし、ユニット自体と同じバグが発生しやすいようです。しかし、その一方で、自信を持ってリファクタリングでき、テストケースが印刷され、テストが実行されたときに問題がないことを確認できます。

1
Adam Thompson

実際には、コードには、意味のある名前を持つ実際のメソッド、意味のある名前を持つ実際のパラメーター、および実行時の実際の影響が含まれます。単体テストでテストするのは実際の影響であり、いくつかの抽象的なfoobar if条件の結果ではありません。

各ユニットテストは、6つではなく1つだけをテストする必要があります。ifステートメントを使用してコードにブランチを作成するたびに、新しいブランチを実行するには、1つ以上の新しいテストを作成する必要があります。そうすることで、コードカバレッジが向上し、テストの品質が向上します。

最後に、各テストには、テストする実際の影響を具体化する意味のある名前を付ける必要があります。 InvoiceGrandTotalShouldNotBeNegativeなど。テストに名前を付けるための適切な方法については、 here を参照してください。

5
Robert Harvey

単体テストでテストされているロジックを繰り返すことは悪い習慣ですか?

いいえ、しかしある程度です。

いいえ、テストは自動化されたチェックであり、テスト対象がより単純なシステムのように動作することを確認します。したがって、解決しようとしている問題が論理ゲートの精巧なダンスを必要とする場合、テストでその同じ論理の影が表示されます。

これは、論理テーブルとしてモデル化できるものを実行しているときに特に当てはまります。その動作を測定するために、テストでは、使用する論理テーブルのコピーが必要になります。

(場合によっては、これにより、論理テーブルを引数として渡す設計に移行します-テーブル自体が単なる大きなデータになります)。

しかし、一種の:多くの場合、ロジックは実際には、より高い意味論的目的への手段です。そのため、テストは実装の詳細ではなく、より高いレベル(実際に必要なことを説明する)で記述する必要があります。つまり、testsは同じままですが、テスト内の項目の名前は変更されます。

ケブリン・ヘニーはこれについていくつかの素晴らしい話をしています

3
VoiceOfUnreason

単体テストは次のように考えてください。

所定の状態アクションが実行されたとき(= /// =)ときその後、期待される結果が得られます。

(参照: Given-When-Then

単体テストは、3つの主要セクションに分かれています。

  • Setup:コードが正しい状態であるか、またはメソッドに渡すオブジェクトを適切に準備する必要があります。つまりコードは特定の状態である必要があります。
  • Execution:これは、テストするメソッドを実際に呼び出している場所です。つまりこれが実行するアクションです。
  • 検証:ここで、実行結果を確認します。予想されるすべての状態を検証できますが、特定の状態に対して正しい答えは1つだけです。つまりこれが結果の状態です

テストする各条件は、個別の単体テストである必要があります。ユニットテストがコードを再実装しているように見える場合、それはおそらくいくつかのことの1つです。

  • あなたのコードは非常に単純です(つまり、あなたの不自然な例)
  • 実装の詳細が多すぎてコードから漏れている(つまり、十分に含まれていない)
  • Edgeケースがありません(つまり、テスト値は通常の動作範囲内にあり、エラー処理は行われていません)。

それは悪いことかもしれませんが、チェックする価値があります。ユニットテストが上記の方法でテストしている限り、テストとコードは十分に異なります。たとえば、speak(true,true)は明確に定義されておらず、期待される値ではない可能性があります。ただし、テストは実装が正しいことを前提としています。代わりに、実際の要件を検証し、その状態に対して予想される正しい答えを入力してください。

私の不自然な例を使用すると、コードは純粋に機能的であるため、指定されたものは暗黙的です。しかし、speak(true,true)は「willkommen」であると想定されています。テストは次のように記述されます。

test('when condition one and condition two', () => {
    expect(speak(true, true)).toEqual('willkommen')
}

そして、それを実現するにはコードを修正する必要があります。

0
Berin Loritsch

あなたが尋ねる:1つの単体テスト内

  • 条件付きのテストであるP(a, b, c); P(d, e, f); P(g, h, i);を使用できるか、または
  • if (a) P(b, c) else if ...を使用した方がよいでしょう。

単純なif階層は、より読みやすく、より明確にすることができます。ただし、条件が相互依存的で複雑になると、最初のスタイルのほうが魅力的になります。

また、複数のassert /呼び出しが発生する可能性もありますが、そうでない場合、キャプチャされないロジックになります。

実際、一般的な"1つのテスト-1つのテスト済み呼び出し"パラダイムとは対照的に、ヘルパー関数(テストデータの構築)を使用して、より多くのケースが実を結んで、ヘルパー関数を複数回呼び出しています。

  1. テストの失敗は正確であり、エラーを示しています。
  2. テストコードは読みやすく、整理されています。 (30個のテストが7-8-5-10の類似したグループに分割されるわけではありません。)
  3. メンテナンスが良いようです。

したがって、一般的な規則にもかかわらず、ほとんど宣言ケースのリストが私の好みです。しかし、私はここでポイントを獲得することを期待していません。

0
Joop Eggen