コードの単体テストを作成し、単体テストのロジックの一部または多くを繰り返して単体テストを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)
})
})
これにより、テーブルを手動で書き出すのが面倒な場合のテストがはるかに簡単になりました。ただし、ユニット自体と同じバグが発生しやすいようです。しかし、その一方で、自信を持ってリファクタリングでき、テストケースが印刷され、テストが実行されたときに問題がないことを確認できます。
実際には、コードには、意味のある名前を持つ実際のメソッド、意味のある名前を持つ実際のパラメーター、および実行時の実際の影響が含まれます。単体テストでテストするのは実際の影響であり、いくつかの抽象的なfoobar if条件の結果ではありません。
各ユニットテストは、6つではなく1つだけをテストする必要があります。if
ステートメントを使用してコードにブランチを作成するたびに、新しいブランチを実行するには、1つ以上の新しいテストを作成する必要があります。そうすることで、コードカバレッジが向上し、テストの品質が向上します。
最後に、各テストには、テストする実際の影響を具体化する意味のある名前を付ける必要があります。 InvoiceGrandTotalShouldNotBeNegative
など。テストに名前を付けるための適切な方法については、 here を参照してください。
単体テストでテストされているロジックを繰り返すことは悪い習慣ですか?
いいえ、しかしある程度です。
いいえ、テストは自動化されたチェックであり、テスト対象がより単純なシステムのように動作することを確認します。したがって、解決しようとしている問題が論理ゲートの精巧なダンスを必要とする場合、テストでその同じ論理の影が表示されます。
これは、論理テーブルとしてモデル化できるものを実行しているときに特に当てはまります。その動作を測定するために、テストでは、使用する論理テーブルのコピーが必要になります。
(場合によっては、これにより、論理テーブルを引数として渡す設計に移行します-テーブル自体が単なる大きなデータになります)。
しかし、一種の:多くの場合、ロジックは実際には、より高い意味論的目的への手段です。そのため、テストは実装の詳細ではなく、より高いレベル(実際に必要なことを説明する)で記述する必要があります。つまり、testsは同じままですが、テスト内の項目の名前は変更されます。
ケブリン・ヘニーはこれについていくつかの素晴らしい話をしています
単体テストは次のように考えてください。
所定の状態、アクションが実行されたとき(= /// =)とき、その後、期待される結果が得られます。
(参照: Given-When-Then )
単体テストは、3つの主要セクションに分かれています。
テストする各条件は、個別の単体テストである必要があります。ユニットテストがコードを再実装しているように見える場合、それはおそらくいくつかのことの1つです。
それは悪いことかもしれませんが、チェックする価値があります。ユニットテストが上記の方法でテストしている限り、テストとコードは十分に異なります。たとえば、speak(true,true)
は明確に定義されておらず、期待される値ではない可能性があります。ただし、テストは実装が正しいことを前提としています。代わりに、実際の要件を検証し、その状態に対して予想される正しい答えを入力してください。
私の不自然な例を使用すると、コードは純粋に機能的であるため、指定されたものは暗黙的です。しかし、speak(true,true)
は「willkommen」であると想定されています。テストは次のように記述されます。
test('when condition one and condition two', () => {
expect(speak(true, true)).toEqual('willkommen')
}
そして、それを実現するにはコードを修正する必要があります。
あなたが尋ねる:1つの単体テスト内
P(a, b, c); P(d, e, f); P(g, h, i);
を使用できるか、またはif (a) P(b, c) else if ...
を使用した方がよいでしょう。単純なif階層は、より読みやすく、より明確にすることができます。ただし、条件が相互依存的で複雑になると、最初のスタイルのほうが魅力的になります。
また、複数のassert /呼び出しが発生する可能性もありますが、そうでない場合、キャプチャされないロジックになります。
実際、一般的な"1つのテスト-1つのテスト済み呼び出し"パラダイムとは対照的に、ヘルパー関数(テストデータの構築)を使用して、より多くのケースが実を結んで、ヘルパー関数を複数回呼び出しています。
したがって、一般的な規則にもかかわらず、ほとんど宣言ケースのリストが私の好みです。しかし、私はここでポイントを獲得することを期待していません。