数値が意味をなさない場合、単体テストでマジックナンバーを使用できますか?


58

単体テストでは、コードに任意の値をスローして、その機能を確認することがよくあります。たとえば、foo(1, 2, 3)17を返すことになっていることがわかっている場合、次のように記述できます。

assertEqual(foo(1, 2, 3), 17)

これらの数値は純粋に任意であり、より広い意味を持ちません(たとえば、境界条件ではありませんが、これらについてもテストします)。私はこれらの数字の良い名前を思い付くのに苦労するだろうし、そのような何かを書くことconst int TWO = 2;は明らかに役に立たない。このようなテストを書いても大丈夫ですか?それとも定数に数値を含める必要がありますか?

されているすべてのマジックナンバーは同じを作成しましたか?、文脈から意味が明らかな場合、マジックナンバーはOKであることがわかりましたが、この場合、実際にはナンバーはまったく意味を持ちません。


9
値を入れて、同じ値を読み返すことができると期待している場合は、マジックナンバーで十分だと思います。たとえば、1, 2, 3以前にvalueを格納した3D配列インデックスがある場合17、このテストは(いくつかのネガティブテストがある限り)ダンディになると思います。ただし、計算の結果である場合は、このテストを読んでいる人がなぜそうなのかを理解し、マジックナンバーはおそらくその目標を達成しないことを確認するfoo(1, 2, 3)必要が17あります。
ジョーホワイト

24
const int TWO = 2;を使用するよりもさらに悪い2。ルールの精神に違反することを意図したルールの文言に準拠しています。
Agent_L

4
「何も意味しない」数字とは何ですか?何も意味がないのに、なぜコードに含まれているのでしょうか?
ティムグラント

6
承知しました。このような一連のテストの前に、たとえば「手動で決定された例の小さな選択」などのコメントを残してください。これは、境界と例外を明確にテストしている他のテストとの関係で明確になります。
davidbak

5
あなたの例は誤解を招く-あなたの関数名が本当にあるfoo場合、それは何も意味しないので、パラメータです。しかし、現実には、私は、関数がその名前を持っていないかなり確信して、パラメータは名前を持っていないbar1bar2bar3。名前は、より現実的な例作る持っている 意味が、それは、テストデータ値が名前を必要とする場合も、議論するためにはるかに理にかなっています。
Doc Brown

回答:


80

本当に意味のない数字があるのはいつですか?

通常、数値に意味がある場合は、テストメソッドのローカル変数に割り当てて、コードを読みやすく、わかりやすいものにする必要があります。変数の名前は、少なくとも変数の意味を反映している必要があり、必ずしもその値を反映していません。

例:

const int startBalance = 10000;
const float interestRate = 0.05f;
const int years = 5;

const int expectedEndBalance = 12840;

assertEqual(calculateCompoundInterest(startBalance, interestRate, years),
            expectedEndBalance);

最初の変数には名前HUNDRED_DOLLARS_ZERO_CENTが付けられてstartBalanceいませんが、変数の意味を示すためであり、その値が特別なものではないことに注意してください。


3
@Kevin-どの言語でテストしていますか?いくつかのテストフレームワークを使用すると、テストのための値の配列の配列を返すデータプロバイダ設定してみましょう
HorusKol

10
私はその考えに同意しますが、誤ってaのような値を抽出した場合のよう0.05fに、この方法でも新しいエラーが発生する可能性があることに注意してくださいint。:)
ジェフボウマン

5
+1-すばらしいもの。特定の値が何であるかを気にしないからといって、それがまだ魔法の数字ではないというわけではありません...
ロビーディー

2
@PieterB:AFAIKは、const変数の概念を正式にしたCおよびC ++の欠点です。
スティーブジェソップ

2
変数に名前を付けたパラメーターと同じ名前を付けましたcalculateCompoundInterestか?その場合、追加の型付けは、テストしている関数のドキュメントを読んだか、少なくともIDEによって指定された名前をコピーしたことの証明です。これが読者にコードの意図をどれだけ伝えているかはわかりませんが、少なくともパラメーターを間違った順序で渡すと、意図したことを伝えることができます。
スティーブジェソップ

20

任意の数字を使用してその動作を確認する場合、実際に探しているのは、おそらくランダムに生成されたテストデータ、またはプロパティベースのテストです。

たとえば、Hypothesisはこの種のテスト用のクールなPythonライブラリであり、QuickCheckに基づいています

通常の単体テストは次のようなものだと考えてください。

  1. いくつかのデータを設定します。
  2. データに対していくつかの操作を実行します。
  3. 結果について何かを主張します。

仮説により、代わりに次のようなテストを作成できます。

  1. 何らかの仕様に一致するすべてのデータについて。
  2. データに対していくつかの操作を実行します。
  3. 結果について何かを主張します。

アイデアは、自分自身の値に制約するのではなく、関数が仕様に一致することを確認するために使用できるランダムな値を選択することです。重要な注意事項として、これらのシステムは通常、失敗した入力を記憶し、それらの入力が将来的に常にテストされるようにします。

ポイント3は一部の人々にとって混乱を招く可能性があるため、明確にしましょう。それはあなたが正確な答えを主張しているという意味ではありません-これは明らかに任意の入力に対して行うことは不可能です。代わりに、結果のプロパティについて何かを主張します。たとえば、リストに何かを追加した後、リストが空にならない、または自己分散バイナリ検索ツリーが実際にバランスが取れている(特定のデータ構造の基準を使用して)と主張する場合があります。

全体的に、自分で任意の数字を選ぶことはおそらくかなり悪いです-それは実際に価値の全体の束を追加するわけではなく、それを読む他の人を混乱させます。一連のランダムなテストデータを自動的に生成し、それを効果的に使用するのは良いことです。選択した言語の仮説またはQuickCheckのようなライブラリを見つけることは、おそらく他の人に理解されたまま目標を達成するためのより良い方法です。


11
ランダムテストでは、再現が難しいバグが見つかる場合がありますが、ランダムテストでは再現可能なバグはほとんど見つかりません。特定の再現可能なテストケースでテストの失敗をキャプチャしてください。
JBRウィルキンソン

5
そして、「結果について何かを主張する」(この場合、fooコンピューティングとは何かを再計算する)ときにユニットテストがバグにならないことをどうやって知るのですか?コードが正しい答えを与えると100%確信している場合は、そのコードをプログラムに配置するだけで、テストはしません。そうでない場合は、テストをテストする必要があります。誰もがこれがどこに向かっているのかがわかると思います。

2
ええ、ランダムな入力を関数に渡す場合、出力が正しく機能していると断言できるようにするには、出力が何であるかを知る必要があります。固定/選択されたテスト値を使用すると、もちろん手作業などで解決できますが、結果が正しいかどうかを判断する自動化された方法は、テストしている機能とまったく同じ問題の影響を受けます。持っている実装を使用するか(動作するかどうかをテストしているためできない)、またはバグが発生する可能性が高い新しい実装を作成します)。
クリス

7
@NajibIdrissi-必ずしもではありません。たとえば、テスト対象の操作の逆数を結果に適用すると、開始時の初期値が返されることをテストできます。それとも、期待不変条件(例えば、全く関心の計算のためにテストすることができd日間、計算がでd日+ 1ヶ月は高い知ら毎月の割合率でなければなりません)、など
ジュール・

12
@Chris-多くの場合、結果が正しいことを確認する方が、結果を生成するよりも簡単です。これはすべての状況に当てはまるわけではありませんが、実際には多くのことがあります。例:バランスの取れたバイナリツリーにエントリを追加すると、バランスの取れた新しいツリーが生成されます...テストが簡単で、実際に実装するのは非常に難しいです。
ジュール

11

単体テスト名は、ほとんどのコンテキストを提供する必要があります。定数の値からではありません。テストの名前/ドキュメントには、テスト内に存在するマジックナンバーの適切なコンテキストと説明を提供する必要があります。

それで十分でない場合は、わずかなドキュメントで(変数名またはdocstringを使用して)提供できるはずです。関数自体には、できれば意味のある名前を持つパラメーターがあることを覚えておいてください。これらをテストにコピーして引数に名前を付けることは、無意味です。

最後に、ユニットテストが非常に複雑で、これが難しい/実用的でない場合、おそらく機能が複雑すぎて、なぜそうなのかを考えるかもしれません。

テストをだらだらと書くほど、実際のコードは悪くなります。テストを明確にするためにテスト値に名前を付ける必要があると感じた場合、実際のメソッドにはより良い命名および/またはドキュメントが必要であることを強く示唆します。テストで定数に名前を付ける必要がある場合は、なぜこれが必要なのかを調べます-問題はおそらくテスト自体ではなく実装です


この答えは...実際の問題は、メソッドのパラメータにマジックナンバーについてであるのに対し、テストの目的を推測することの難しさについてのように見える
ロビーディー

@RobbieDeeテストの名前/ドキュメントは、テスト内に存在するマジックナンバーの適切なコンテキストと説明を提供する必要があります。そうでない場合は、ドキュメントを追加するか、テストの名前を変更してわかりやすくします。
エンダーランド

それでも、マジックナンバーに名前を付ける方が良いでしょう。パラメータの数が変更されると、ドキュメントが古くなるリスクがあります。
ロビーディー

1
@RobbieDeeは、関数自体に、できれば意味のある名前を持つパラメーターがあることを忘れないでください。これらをテストにコピーして引数に名前を付けることは、無意味です。
エンダーランド

「願わくば」は?フィリップがすでに概説したように、単に適切にコーディングし、表面上はマジックナンバーであるものを廃止しないのはなぜですか?
ロビーディー

9

これは、テストする機能に大きく依存します。私は、個々の数字がそれ自体で特別な意味を持たない多くの場合を知っていますが、テストケース全体は思慮深く構築されているので、特定の意味を持っています。それは何らかの方法で文書化する必要があるものです。たとえば、3つの数値が三角形のエッジの有効な長さであるかどうかをfoo実際testForTriangleに決定する方法である場合、テストは次のようになります。

// standard triangle with area >0
assertEqual(testForTriangle(2, 3, 4), true);

// degenerated triangle, length of two edges match the length of the third
assertEqual(testForTriangle(1, 2, 3), true);  

// no triangle
assertEqual(testForTriangle(1, 2, 4), false); 

// two sides equal
assertEqual(testForTriangle(2, 2, 3), true);

// all three sides equal
assertEqual(testForTriangle(4, 4, 4), true);

// degenerated triangle / point
assertEqual(testForTriangle(0, 0, 0), true);  

等々。これを改善し、コメントをメッセージパラメータに変換してassertEqual、テストが失敗したときに表示されるメッセージパラメータにすることができます。その後、これをさらに改善し、データ駆動型テストにリファクタリングできます(テストフレームワークがこれをサポートしている場合)。それにもかかわらず、この数字を選んだ理由と、個々のケースでテストしているさまざまな動作のうちのどれをコードに書き留めれば、あなたは自分自身に恩恵をもたらします。

もちろん、他の関数では、パラメーターの個々の値がより重要になる可能性があるため、パラメーターの意味fooをどのように扱うかを尋ねる場合など、意味のない関数名を使用することはおそらく最善のアイデアではありません。


賢明なソリューション。
user1725145

6

数字の代わりに名前付き定数を使用したいのはなぜですか?

  1. DRY-3つの場所で値が必要な場合、一度だけ定義したいので、変更された場合は1つの場所で変更できます。
  2. 数字に意味を与えます。

複数のユニットテストを作成し、それぞれに3つの数字(startBalance、interest、years)の品揃えがある場合、ローカル変数として値をユニットテストにパックします。それらが属する最小のスコープ。

testBigInterest()
  var startBalance = 10;
  var interestInPercent = 100
  var years = 2
  assert( calcCreditSum( startBalance, interestInPercent, years ) == 40 )

testSmallInterest()
  var startBalance = 50;
  var interestInPercent = .5
  var years = 1
  assert( calcCreditSum( startBalance, interestInPercent, years ) == 50.25 )

名前付きパラメーターを許可する言語を使用している場合、これはもちろん大げさです。そこで、メソッド呼び出しで生の値をパックします。このステートメントをより簡潔にするリファクタリングは想像できません。

testBigInterest()
  assert( calcCreditSum( startBalance:       10
                        ,interestInPercent: 100
                        ,years:               2 ) = 40 )

または、テストフレームワークを使用します。これにより、テストケースを配列またはマップ形式で定義できます。

testcases = { {
                Name: "BigInterest"
               ,StartBalance:       10
               ,InterestInPercent: 100
               ,Years:               2
              }
             ,{ 
                Name: "SmallInterest"
               ,StartBalance:       50
               ,InterestInPercent:  .5
               ,Years:               1
              }
            }

3

...しかし、この場合、数字は実際にはまったく意味を持ちません

番号はメソッドを呼び出すために使用されているため、上記の前提は間違いです。あなたは数字が何であるか気にしないかもしれませんが、それはポイントの横にあります。はい、いくつかのIDEウィザードで使用されている数値を推測できますが、値に名前を付けた方が、パラメーターに一致する場合でもはるかに優れています。


1
ただし、これは必ずしも真実ではありません-私が書いた最新の単体テストの例のように(assertEqual "Returned value" (makeKindInt 42) (runTest "lvalue_operators"))。この例で42は、は、名前の付いたテストスクリプトのコードによって生成さlvalue_operatorsれ、スクリプトから返されたときにチェックされる単なるプレースホルダー値です。同じ値が2つの異なる場所で発生すること以外は、まったく意味がありません。ここで実際に有用な意味を与える適切な名前は何でしょうか?
ジュール

3

境界条件ではない入力のセットで純関数をテストする場合、ほぼ確実に境界条件ではない(および境界条件である)入力のセット全体でテストする必要があります。そして、私にとっては、関数を呼び出す値のテーブルとループが必要であることを意味します:

struct test_foo_values {
    int bar;
    int baz;
    int blurf;
    int expected;
};
const struct test_foo_values test_foo_with[] = {
   { 1, 2, 3, 17 },
   { 2, 4, 9, 34 },
   // ... many more here ...
};

for (size_t i = 0; i < ARRAY_SIZE(test_foo_with); i++) {
    const struct test_foo_values *c = test_foo_with[i];
    assertEqual(foo(c->bar, c->baz, c->blurf), c->expected);
}

Dannnnoの答えで提案されているようなツールは、テストする値のテーブルを作成するのに役立ちます。 barbaz、及びblurfで説明したように意味のある名前に置き換える必要があるフィリップの答え

(ここでの議論の一般原則:数字は常に名前を必要とする「魔法の数字」ではなく、代わりに数字がデータである可能性があります。 。逆に、データが手元にあると思われる場合は、データを配列に入れ、さらにデータを取得することを検討してください。


1

テストは製品コードとは異なり、少なくともSpockで記述されたユニットテストでは短く、要点は、マジック定数を使用しても問題ありません。

テストが5行の長さで、指定された/ when / thenの基本スキームに従っている場合、そのような値を定数に抽出すると、コードが長くなり読みにくくなります。「Smithという名前のユーザーを追加すると、ユーザーリストにSmithというユーザーが返されます」というロジックの場合、定数に「Smith」を抽出しても意味がありません。

もちろん、これは、 "given"(セットアップ)ブロックで使用される値を "when"および "then"ブロックで見つかった値と簡単に一致させることができる場合に適用されます。テストのセットアップが、データが使用される場所から(コードで)分離されている場合、定数を使用する方が良いかもしれません。しかし、テストは自己完結型であるため、セットアップは通常使用場所に近く、最初のケースが適用されます。つまり、この場合、マジック定数は非常に受け入れられます。


1

まず、プログラマーが作成するすべての自動化されたテストをカバーするために「ユニットテスト」がよく使用され、各テストの名前を議論するのは無意味であることに同意しましょう。

私は、ソフトウェアが多くの入力を受け取り、他の数値を最適化しながらいくつかの制約を満たさなければならない「解決策」を作成するシステムに取り組んできました。 正しい答えはなかったので、ソフトウェアは合理的な答えを出すだけでした。

これは、多くの乱数を使用して開始点を取得し、「ヒルクライマー」を使用して結果を改善することで実現しました。これは何度も実行され、最良の結果が得られました。乱数ジェネレーターをシードできるため、常に同じ番号が同じ順序で出力されるため、テストでシードを設定すると、実行ごとに結果が同じになることがわかります。

上記を行う多くのテストを行い、結果が同じであることを確認しました。これは、システムのその部分がリファクタリングなどで誤って行ったことを変更していないことを示しています。システムのその部分がしたこと。

これらのテストは、最適化コードを変更するとテストが中断されるため、維持に費用がかかりますが、データを前処理し、結果を後処理する非常に大きなコードでバグを発見しました。

データベースを「モック」したので、これらのテストを「ユニットテスト」と呼ぶことができますが、「ユニット」はかなり大きかったです。

多くの場合、テストのないシステムで作業しているときは、上記のようなことを行い、リファクタリングが出力を変更しないことを確認できるようにします。うまくいけば、新しいコード用のより良いテストが作成されます!


1

この場合、番号はマジック番号ではなく、任意番号と呼ばれるべきであり、その行を「任意のテストケース」としてコメントする必要があると思います。

もちろん、いくつかのマジックナンバーは、一意の「ハンドル」値(もちろん、名前付き定数に置き換える必要があります)に関しては任意ですが、「2週間ごとのハロンのヨーロッパのすずめの対気速度」などの事前計算された定数にすることもできます。コメントや有用なコンテキストなしで数値がプラグインされます。


0

決定的な「はい/いいえ」と言う限り、私はベンチャーしませんが、ここで、大丈夫かどうかを決める際に自問すべき質問がいくつかあります。

  1. 数字が意味をなさない場合、そもそもなぜそこにあるのですか?他のものに置き換えることはできますか?値の表明の代わりにメソッド呼び出しとフローに基づいて検証を行うことはできますか?verify()実際に値をアサートする代わりに、特定のメソッド呼び出しがモックオブジェクトに対して行われたかどうかをチェックするMockitoのメソッドのようなものを考えてください。

  2. 数字何かを意味する場合は、適切な名前の変数に割り当てる必要があります。

  3. 特定のコンテキストでは役に立つかもしれないが、他のコンテキストではそれほどではないような数字2を書くTWO

    • たとえばassertEquals(TWO, half_of(FOUR))、コードを読む人にとって意味があります。をテストしているのかすぐにわかります。
    • あなたのテストがあるしかし、もしassertEquals(numCustomersInBank(BANK_1), TWO)、これは行わないことあまり意味。なぜBANK_1 2人の顧客が含まれているのですか?たちはをテストしていますか?
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.