テストデータをランダムに生成することは悪い習慣ですか?
質問
rspecを使い始めてから、フィクスチャの概念に問題がありました。私の主な関心事はこれです:
-
私はテストを使用して、驚くべき動作を明らかにしています。私がテストしている例のあらゆる可能性のあるエッジケースを列挙するのに十分なほど賢いわけではありません。ハードコード化されたフィクスチャの使用は、想像した非常に具体的なケースでのみコードをテストするため、制限されているようです。 (確かに、私の想像力は、どのケースをテストするかに関しても制限されています。)
-
コードのドキュメント形式としてテストを使用しています。フィクスチャ値をハードコーディングしている場合、特定のテストが実証しようとしているものを明らかにするのは困難です。例:
describe Item do describe '#most_expensive' do it 'should return the most expensive item' do Item.most_expensive.price.should == 100 # OR #Item.most_expensive.price.should == Item.find(:expensive).price # OR #Item.most_expensive.id.should == Item.find(:expensive).id end end end
最初の方法を使用すると、最も高価なアイテムが何であるかを読者に示すことはできません。価格は100だけです。3つの方法はすべて、フィクスチャー
:expensive
がfixtures / items.yml
にリストされている最も高価なもの。不注意なプログラマーは、before(:all)
でItem
を作成するか、別のフィクスチャーをfixtures / items.yml
に挿入することでテストを中断できます。それが大きなファイルである場合、問題が何であるかを理解するのに長い時間がかかる可能性があります。
やろうとしていることの1つは、すべてのモデルに #generate_random
メソッドを追加することです。このメソッドは、仕様を実行しているときにのみ使用できます。例:
class Item
def self.generate_random(params={})
Item.create(
:name => params[:name] || String.generate_random,
:price => params[:price] || rand(100)
)
end
end
(これを行う方法の具体的な詳細は実際には少しきれいです。すべてのモデルの生成とクリーンアップを処理するクラスがありますが、このコードは私の例にとって十分に明確です。)上記の例では、次のようにテストできます。心臓の弱者への警告:私のコードは before(:all)
:
describe Item do
describe '#most_expensive' do
before(:all) do
@items = []
3.times { @items << Item.generate_random }
@items << Item.generate_random({:price => 50})
end
it 'should return the most expensive item' do
sorted = @items.sort { |a, b| b.price <=> a.price }
expensive = Item.most_expensive
expensive.should be(sorted[0])
expensive.price.should >= 50
end
end
end
このようにして、私のテストは驚くべき振る舞いをよりよく明らかにします。この方法でデータを生成すると、コードが期待どおりに動作しないが、フィクスチャのみを使用している場合はキャッチできなかったエッジケースにたまに出くわします。たとえば、 #most_expensive
の場合、複数のアイテムが最も高価な価格を共有する特別なケースを処理するのを忘れると、最初の should
でテストが失敗することがあります。 AutoSpecで非決定論的な失敗を見ると、何かが間違っているという手がかりになります。フィクスチャのみを使用していた場合、そのようなバグを発見するのにはるかに時間がかかる可能性があります。
また、私のテストは、コードで予想される動作がどのようなものであるかを示すためのわずかに良い仕事をしています。私のテストでは、sortedは価格の降順でソートされたアイテムの配列であることを明確にしています。 #most_expensive
がその配列の最初の要素と等しいことを期待しているため、 most_expensive
の予想される動作が何であるかはさらに明白です。
それで、これは悪い習慣ですか?備品に対する私の恐怖は不合理なものですか?モデルごとに generate_random
メソッドを書くのはやり過ぎですか?または、これは機能しますか?
解決
これは2番目のポイントに対する答えです。
(2)コードのドキュメントの形式としてテストを使用しています。フィクスチャ値をハードコーディングしている場合、特定のテストが実証しようとしているものを明らかにするのは困難です。
同意します。理想的には、仕様例はそれ自体で理解できるはずです。フィクスチャを使用すると、例の前提条件が期待される結果から分割されるため、問題があります。
このため、多くのRSpecユーザーはフィクスチャの使用を完全に停止しました。代わりに、仕様例自体に必要なオブジェクトを構築します。
describe Item, "#most_expensive" do
it 'should return the most expensive item' do
items = [
Item.create!(:price => 100),
Item.create!(:price => 50)
]
Item.most_expensive.price.should == 100
end
end
オブジェクトを作成するための定型コードが大量にある場合は、 factory_girl 、機械工、または< a href = "http://replacefixtures.rubyforge.org/" rel = "nofollow noreferrer"> FixtureReplacement 。
他のヒント
このトピックまたはに誰もリンクしていないことに驚いています言及 モンテカルロテスト。これは、ランダム化されたテスト入力を広範囲に使用した唯一の時間です。ただし、テストケースごとに乱数ジェネレーターのシードを一定にすることで、テストを再現可能にすることが非常に重要でした。
最近の私のプロジェクトで、このことについて多くのことを考えました。最後に、2つのポイントに決めました:
- テストケースの再現性は非常に重要です。ランダムテストを作成する必要がある場合は、広範囲に文書化する準備をしてください。失敗した場合/失敗した場合は、正確な理由を知る必要があるためです。
- コードカバレッジの松葉杖としてランダムネスを使用するということは、カバレッジが不十分であるか、代表的なテストケースを構成するドメインを十分に理解していないことを意味します。どちらが正しいかを把握し、それに応じて修正してください。
要するに、ランダム性は多くの場合、それが価値があるよりも厄介なことがあります。トリガーを引く前に、正しく使用するかどうかを慎重に検討してください。最終的に、ランダムテストケースは一般的に悪い考えであり、使用する場合は控えめに使用することを決定しました。
多くの良い情報がすでに投稿されていますが、以下も参照してください:ファズテスト。通りの言葉は、マイクロソフトが多くのプロジェクトでこのアプローチを使用していることです。
テストの私の経験は主にC / Python / Javaで書かれた単純なプログラムであるため、これが完全に適用できるかどうかはわかりませんが、あらゆる種類のユーザー入力を受け入れることができるプログラムがあるときは常に、ランダムな入力データ、または少なくとも予測できない方法でコンピューターによって生成された入力データを使用したテスト。ユーザーが入力するものについて推測できないためです。または、まあ、あなたは できますが、そうすると、その仮定を行わないハッカーがあなたが完全に見落としたバグを見つけるかもしれません。マシンで生成された入力は、テスト手順から人間の偏見を完全に排除するために私が知っている最良の(唯一の?)方法です。もちろん、失敗したテストを再現するためには、テストを実行する前に、テスト入力をファイルに保存するか、印刷する(テキストの場合)などの操作を行う必要があります。
ランダムなテストは、 oracleの問題の解決策がない限り、つまり、入力が与えられたソフトウェアの予想結果がどれであるかを判断する限り、悪い習慣です。
オラクルの問題を解決した場合、単純なランダム入力の生成よりも一歩先を行くことができます。ソフトウェアの特定の部分が単純なランダムよりも多く行使されるように、入力分布を選択できます。
その後、ランダムテストから統計テストに切り替えます。
if (a > 0)
// Do Foo
else (if b < 0)
// Do Bar
else
// Do Foobar
a
と b
を int
の範囲でランダムに選択すると、50%の確率で Foo
を行使します、 Bar
25%の時間、 Foobar
25%の時間。 Bar
や Foobar
よりも Foo
の方が多くのバグを見つける可能性があります。
a
を選択して、66.66%の負の時間になると、 Bar
と Foobar
が最初の配布よりも多く実行されます。実際、3つのブランチはそれぞれ33.33%の時間で行使されます。
もちろん、観察された結果が予想された結果と異なる場合、バグを再現するのに役立つ可能性のあるすべてのものをログに記録する必要があります。
マシニストをご覧になることをお勧めします:
Machinistがデータを生成しますが、繰り返し可能であるため、各テスト実行には同じランダムデータが含まれます。
一貫して乱数ジェネレータをシードすることで、同様のことができます。
ランダムに生成されたテストケースの1つの問題は、コードによって回答を検証する必要があり、バグがないことを確認できないことです:)
このトピックも表示される場合があります。ランダム入力を使用したテストのベストプラクティス 。
このようなテストの有効性は、使用する乱数ジェネレーターの品質と、RNGの出力をテストデータに変換するコードの正確さに大きく依存します。
RNGが値を生成せず、コードが何らかのエッジケース条件に陥る場合、このケースはカバーされません。 RNGの出力をテストするコードの入力に変換するコードに欠陥がある場合、優れたジェネレーターを使用しても、すべてのエッジケースにヒットしないことがあります。
どのようにテストしますか?
テストケースのランダム性の問題は、出力がランダムであることです。
テスト(特に回帰テスト)の背後にある考え方は、何も壊れていないことを確認することです。
壊れたものを見つけた場合は、それ以降毎回そのテストを含める必要があります。そうしないと、一貫した一連のテストができなくなります。また、機能するランダムテストを実行する場合は、そのテストを含める必要があります。テストが失敗するようにコードを壊す可能性があるためです。
つまり、その場で生成されたランダムデータを使用するテストがある場合、これは悪い考えだと思います。ただし、保存して再利用する一連のランダムデータを使用する場合は、これが良い考えです。これは、乱数ジェネレーターのシードのセットの形式をとることができます。
生成されたデータのこの保存により、このデータに対する「正しい」応答を見つけることができます。
したがって、ランダムデータを使用してシステムを探索することをお勧めしますが、テストでは定義済みデータ(元はランダムに生成されたデータである可能性があります)を使用します
ランダムテストデータの使用は優れたプラクティスです。ハードコードされたテストデータは明示的に考えたケースのみをテストしますが、ランダムデータは間違っているかもしれない暗黙の仮定をフラッシュします。
このためにFactory Girlとffakerを使用することを強くお勧めします。 (どんな状況でも、Railsのフィクスチャを使用しないでください。)