web-dev-qa-db-ja.com

ダイスを転がすユースケースをカバーするための良いユニットテストは何ですか?

私はユニットテストを理解しようとしています。

たとえば、デフォルトの面の数が6のサイコロがあるとします(ただし、4面、5面など)。

import random
class Die():
    def __init__(self, sides=6):
        self._sides = sides

    def roll(self):
        return random.randint(1, self._sides)

以下は有効/有用な単体テストでしょうか?

  • 6面ダイスのロールを1〜6の範囲でテストする
  • 6面のサイコロに対して0のロールをテストする
  • 6面ダイスに対して7のロールをテストする
  • 1〜3の範囲のロールを3面サイコロでテストします。
  • 3面のサイコロに対して0のロールをテストする
  • 3面のサイコロに対して4のロールをテストする

ランダムモジュールが十分に長い間使用されているので、これらは時間の無駄だと思っていますが、ランダムモジュールが更新されるかどうかを考えます(たとえば、Python version)を更新します)。少なくとも私はカバーされています。

また、私はダイスロールの他のバリエーションをテストする必要さえありますか?この場合の3、または別の初期化されたダイの状態をカバーするのは良いですか?

18
Cybran

そうです、あなたのテストはrandomモジュールがその仕事をしていることを確認すべきではありません。 unittestはクラス自体のみをテストする必要があり、他のコードとの相互作用(個別にテストする必要はありません)ではありません。

もちろん、コードが誤ってrandom.randint()を使用している可能性は十分にあります。または、代わりにrandom.randrange(1, self._sides)を呼び出して、サイコロが最高値をスローすることは決してありませんが、それは別の種類のバグであり、単体テストではキャッチできません。その場合、dieunitは設計どおりに機能していますが、設計自体に欠陥があります。

この場合、randint()関数をreplaceするためにモックを使用し、それがと呼ばれていることだけを確認します正しく。 Python 3.3以降には、このタイプのテストを処理するための _unittest.mock_モジュール が付属していますが、外部をインストールできます mockパッケージ 完全に同じ機能を取得するための古いバージョン

_import unittest
try:
    from unittest.mock import patch
except ImportError:
    # < python 3.3
    from mock import patch


@patch('random.randint', return_value=3)
class TestDice(unittest.TestCase):
    def _make_one(self, *args, **kw):
        from die import Die
        return Die(*args, **kw)

    def test_standard_size(self, mocked_randint):
        die = self._make_one()
        result = die.roll()

        mocked_randint.assert_called_with(1, 6)
        self.assertEqual(result, 3)

    def test_custom_size(self, mocked_randint):
        die = self._make_one(sides=42)
        result = die.roll()

        mocked_randint.assert_called_with(1, 42)
        self.assertEqual(result, 3)


if __name__ == '__main__':
    unittest.main()
_

モッキングにより、テストは非常に簡単になりました。本当に2ケースしかない。 6面ダイのデフォルトケースとカスタムサイドケース。

Dieのグローバル名前空間のrandint()関数を一時的に置き換える方法は他にもありますが、mockモジュールがこれを最も簡単にします。 _@mock.patch_デコレータ ここでは、テストケースのallテストメソッドに適用されます。各テストメソッドには追加の引数であるモックrandom.randint()関数が渡されるため、モックに対してテストして、実際に正しく呼び出されているかどうかを確認できます。 _return_value_引数は、呼び出されたときにモックから返されるものを指定します。したがって、die.roll()メソッドが実際に「ランダム」な結果を返したことを確認できます。

ここでは別のPythonユニットテストのベストプラクティスを使用しました:テストの一部としてテスト中のクラスをインポートします。__make_one_メソッドはインポートとインスタンス化の作業を行いますテスト内。元のモジュールのインポートを妨げる構文エラーやその他のミスをした場合でも、テストモジュールがロードされます。

このようにして、モジュールのコード自体に誤りがあった場合でも、テストは実行されます。失敗するだけで、コードのエラーについて通知されます。

明確にするために、上記のテストは極端に単純化されています。ここでの目標は、たとえばrandom.randint()が正しい引数で呼び出されたことをテストすることではありません。代わりに、特定の入力が与えられたときにユニットが正しい結果を生成していることをテストすることが目標です。これらの入力には、テスト中の他のユニットの結果が含まれますnotrandom.randint()メソッドをモックすることで、コードへの別の入力だけを制御できます。

real worldテストでは、テスト対象のユニットの実際のコードはより複雑になります。 APIに渡される入力との関係や、他のユニットがどのように呼び出されるかは興味深いものであり、モッキングにより中間結果にアクセスできるほか、これらの呼び出しの戻り値を設定できます。

たとえば、サードパーティのOAuth2サービス(マルチステージインタラクション)に対してユーザーを認証するコードでは、コードがそのサードパーティのサービスに適切なデータを渡していることをテストし、それによってさまざまなエラー応答を模擬することができます。サードパーティのサービスが復活し、完全なOAuth2サーバーを自分で構築しなくても、さまざまなシナリオをシミュレートできます。ここでは、最初の応答からの情報が正しく処理され、第2段階の呼び出しに渡されていることをテストすることが重要です。そのため、モックされたサービスが正しく呼び出されていることを確認する必要があります。

22
Martijn Pieters

Martijnの答え は、random.randintを呼び出していることを示すテストを本当に実行したい場合の方法です。しかし、「質問に答えられない」と言われる危険性があるため、これはユニットテストではまったくすべきではないと感じています。 randintのモックはもはやブラックボックステストではありません-実装で特定のことが起こっていることを具体的に示しています。オプションではなく、ブラックボックステスト-結果が決して1未満になることを証明する実行可能なテストはありません6より。

randintをあざけることはできますか?はい、できます。しかし、あなたは何を証明していますか?引数1と辺でそれを呼び出したこと。 thatはどういう意味ですか?あなたは正方形に戻ります-結局あなたは証明する必要があります- 正式に または非公式に-random.randint(1, sides)の呼び出しが正しく実装されていることサイコロを振る。

私はすべてユニットテストに参加しています。それらは素晴らしい健全性チェックであり、バグの存在を明らかにします。ただし、それらが存在しないことを証明することはできません。また、テストを通じてアサートできないものもあります(たとえば、特定の関数が例外をスローしたり、常に終了したりすることはありません)。利得。確定的な動作の場合、ユニットテストは理にかなっています。なぜなら、期待する答えが実際にわかるからです。

16
Doval

ランダムシードを修正します。 1、2、5、12面のサイコロの場合、数千のロールが1とNを含み、0またはN + 1を含まない結果になることを確認します。予想される範囲をカバーするには、別のシードに切り替えます。

モックツールは便利ですが、ツールを使用して操作を実行できるからといって、操作を実行する必要があるとは限りません。 YAGNIは、機能だけでなくテストフィクスチャにも適用されます。

モックされていない依存関係で簡単にテストできる場合は、ほとんどの場合常にそうするべきです。このようにして、テストはテスト数を増やすだけでなく、欠陥数を減らすことに焦点を当てます。過度のモックリスクは誤解を招くカバレッジの数値を作成し、実際のテストを後のフェーズに延期する可能性があります。

6
soru

あなたがそれについて考えるなら、Dieは何ですか? -randomのラッパーにすぎません。 random.randintを使用して、アプリケーション自体の語彙の観点からラベルを付け直します:Die.Roll

Dierandomの間に別の抽象化レイヤーを挿入しても意味がないと思いますDie自体はすでにこの間接的なレイヤーですアプリケーションとプラットフォーム。

サイコロの缶詰の結果が必要な場合は、_Dieをモックするだけで、randomはモックしないでください。

一般に、外部システムと通信するラッパーオブジェクトを単体テストするのではなく、それらの統合テストを作成します。 Die用にそれらをいくつか書くこともできますが、指摘したように、基礎となるオブジェクトのランダムな性質のため、それらは意味がありません。さらに、ここには構成やネットワーク通信が含まれていないため、プラットフォームの呼び出し以外にテストすることは多くありません。

=> Dieはほんの数行のコードであり、random自体に比べてロジックがほとんどまたはまったく追加されないことを考えると、その特定の例でのテストはスキップします。

3
guillaume31

潮に逆らって泳ぐ危険にさらされて、私は何年も前にこれまで述べられていない方法を使用してこの正確な問題を解決しました。

私の戦略は、RNGを、空間全体に及ぶ予測可能な値のストリームを生成するものでモックすることでした。 (たとえば)side = 6で、RNGが0から5までの値を順番に生成する場合、クラスの動作を予測し、それに応じてユニットテストを行うことができます。

理論的根拠は、RNGが最終的にこれらの値のそれぞれを生成するという前提で、RNG自体をテストせずに、このクラスのロジックのみをテストすることです。

シンプルで、確定的で、再現性があり、バグをキャッチします。私は同じ戦略を再び使用します。


RNGが存在する場合、この質問ではテストの内容を詳しく説明するのではなく、テストに使用できるデータを示します。私の提案は、RNGをモックすることによって徹底的にテストすることです。テストする価値があるかどうかの質問は、質問で提供されていない情報によって異なります。

2
david.pfx

乱数ジェネレータをシードして期待される結果を確認することは、私が知る限り、有効なテストではありません。サイコロが内部でどのように機能するかを想定しています。 python=の開発者は、乱数ジェネレータまたはサイコロを変更できます(注:「サイコロ」は複数、「サイコロ」は単数です。クラスが1回の呼び出しで複数のサイコロを実装しない限り、おそらく「ダイ」と呼ばれる必要があります)、別の乱数ジェネレータを使用できます。

同様に、ランダム関数のモックは、クラス実装が期待どおりに機能すると想定しています。なぜこれが当てはまらないのでしょうか?誰かがデフォルトのpython乱数ジェネレータを制御する可能性があり、それを回避するために、将来のバージョンのダイは複数の乱数またはより大きな乱数をフェッチして、より多くのランダムデータを混合する可能性があります。 FreeBSDオペレーティングシステムのメーカーがNSAがCPUに組み込まれたハードウェア乱数ジェネレーターを改ざんしていると疑ったとき、同様のスキームが使用されました。

もし私だったら、例えば6000回転走って、それらを集計し、1から6までの各数値が500から1500回転することを確認します。また、その範囲外の数値が返されないことも確認します。また、6000ロールの2番目のセットについて、[1..6]を頻度順に注文すると、結果が異なることを確認する場合があります(数値がランダムな場合、これは720回の実行で一度失敗します!)。徹底的に知りたい場合は、1に続く数字の頻度、2に続く数字の頻度などを見つけることができます。ただし、サンプルサイズが十分に大きく、十分な分散があることを確認してください。人間は、乱数のパターンが実際よりも少ないことを期待しています。

12面と2面のダイについて繰り返します(6が最も使用されるため、このコードを書く人にとって最も期待されています)。

最後に、片面ダイ、0面ダイ、-1面ダイ、2.3面ダイ、[1,2,3,4,5,6]面ダイ、および「何とか」側のサイコロ。もちろん、これらはすべて失敗するはずです。彼らは便利な方法で失敗しますか?これらは、ローリングではなく、作成時におそらく失敗するはずです。

または、これらを別の方法で処理したい場合もあるでしょう-[1,2,3,4,5,6]でダイを作成することは許容できるはずです。これは4つの面を持つダイで、各面に文字が書かれている可能性があります。マジックエイトボールと同様に、ゲーム「Boggle」が思い浮かびます。

そして最後に、これを熟考する必要があるかもしれません: http://lh6.ggpht.com/-fAGXwbJbYRM/UJA_31ACOLI/AAAAAAAAAPg/2FxOWzo96KE/s1600-h/random%25255B3%25255D.jpg

2
AMADANON Inc.

あなたの質問で提案するテストは、実装としてモジュラー算術カウンターを検出しません。また、return 1 + (random.randint(1,maxint) % sides)のような確率分布関連コードの一般的な実装エラーを検出しません。または、2次元パターンを生成するジェネレーターへの変更。

均等に分布したランダムに出現する数値を生成していることを実際に確認したい場合は、さまざまなプロパティを確認する必要があります。これでかなり良い仕事をするには、生成された数値に対して http://www.phy.duke.edu/~rgb/General/dieharder.php を実行します。または、同様に複雑な単体テストスイートを作成します。

これは単体テストやTDDのせいではありません。偶然性を検証するのは非常に困難です。そして、例として人気のトピック。

1
Patrick