サイコロを転がすユースケースをカバーするのに適した単体テストとは何ですか?


18

私はユニットテストで把握しようとしています。

デフォルトの辺の数が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のロールをテストします
  • 3面ダイスの1〜3の範囲でロールをテストします。
  • 3面ダイスの0のロールをテストします
  • 3面ダイスの4のロールをテストします

ランダムモジュールが長い間存在していたので、これらは時間の無駄だと思っていますが、ランダムモジュールが更新された場合(たとえば、Pythonバージョンを更新した場合)、少なくともカバーされます。

また、ダイロールの​​他のバリエーション(この場合は3など)をテストする必要もありますか、それとも別の初期化されたダイの状態をカバーするのが良いでしょうか?


1
マイナス5面のダイス、またはヌル面のダイスはどうですか?
JensG

回答:


22

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

もちろん、コードでrandom.randint()間違った使い方をすることは完全に可能です。または、random.randrange(1, self._sides)代わりに呼び出して、ダイスが最高値をスローすることはありませんが、それは別の種類のバグであり、ユニットテストでキャッチできるバグではありません。その場合、die ユニットは設計どおりに動作していますが、設計自体に欠陥がありました。

この場合、モックを使用して関数を置き換えrandint()関数が正しく呼び出されたことを確認するだけです。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面ダイスのデフォルトケース、およびカスタムサイドケース。

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

ここでは、別のPythonユニットテストのベストプラクティスを使用しました。テストの一環として、テスト対象のクラスをインポートします。この_make_oneメソッドはtest内でインポートとインスタンス化を行います。そのため、元のモジュールのインポートを妨げる構文エラーやその他の間違いを犯した場合でも、テストモジュールはロードされます。

このようにして、モジュールコード自体に間違いを犯した場合でも、テストが実行されます。それらは失敗し、コードのエラーについて通知します。

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

では現実世界のテスト、あなたのユニットアンダーテストで実際のコードはより複雑になるだろう。APIに渡される入力と他のユニットがどのように呼び出されるかとの関係は興味深いものになる可能性があり、モッキングにより中間結果へのアクセスが可能になり、それらの呼び出しの戻り値を設定できるようになります。

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


1
2つ以上のテストケースがあります...デフォルト値の結果チェック:下限(1)、上限(6)、下限(0)、上限(7)を超え、max_intなどのユーザー指定の数値の結果等の入力も...いくつかの点でのためにテストする必要があるかもしれない、検証されていません
ジェームズスネル

2
いいえ、これらはrandint()のコードではなくのテストですDie.roll()
マーティンピーターズ14年

実際には、randintだけが正しく呼び出されるのではなく、その結果も正しく使用されるようにする方法があります。sentinel.dieたとえば、それをモックして(センチネルオブジェクトunittest.mockも)を返し、それがロールメソッドから返されたものであることを確認します。これにより、テストされたメソッドを実装する方法は1つだけになります。
aragaer

@aragaer:確かに、値が変更されずに返されることを確認したい場合は、それsentinel.dieを保証する素晴らしい方法でしょう。
マルタインピータース

mocked_randintが特定の値でcalled_withであることを確認したい理由がわかりません。私はrandintをモックして予測可能な値を返すことを望んでいますが、どのような値で呼び出されるのではなく、予測可能な値を返すだけではないのですか?呼び出された値をチェックすることは、テストを実装の詳細に不必要に結び付けているように思えます。また、ダイがrandintの正確な値を返すことに注意する必要があるのはなぜですか?1を超え、最大値以下の値が返されることを本当に気にしませんか?
bdrx

16

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

モックできますrandintか?はい、できます。しかし、あなたは何を証明していますか?引数1とsideで呼び出したこと。何がそれはどういう意味ですか?正方形の1つに戻ります-その日の終わりに、正式にまたは非公式に-呼び出しrandom.randint(1, sides)がサイコロを正しく実装することを証明しなければなりません。

私はすべてユニットテストをしています。それらは素晴らしい健全性チェックであり、バグの存在を明らかにします。しかし、彼らは彼らの不在を決して証明することはできず、テストを通して断言できないものがあります(たとえば、特定の関数が例外をスローしないか、常に終了するなど)。利得。決定論的な振る舞いの場合、単体テストは理にかなっています。なぜなら、あなたが期待している答えが何であるかを実際に知っているからです。


ユニットテストはブラックボックステストではありません。さまざまな部分が設計どおりに相互作用することを確認するために、それが統合テストの目的です。もちろん、それは意見の問題です(ほとんどのテスト哲学は)、「ユニットテスト」はホワイトボックスまたはブラックボックステストに該当するかを参照してくださいそしてブラックボックスユニットテストの一部(スタックオーバーフロー)展望について。
マーティンピーターズ14年

@MartijnPieters「統合テストの目的はこれだ」という意見には同意しません。統合テストは、システムのすべてのコンポーネントが正しく相互作用することを確認するためのものです。これらは、特定のコンポーネントが特定の入力に対して正しい出力を提供することをテストする場所ではありません。ブラックボックスユニットテストとホワイトボックスユニットテストの場合、ホワイトボックスユニットテストは最終的に実装の変更によって中断され、実装で行った仮定はすべてテストに引き継がれます。を使用しrandom.randintて呼び出されることを検証することは1, sides、それが間違っている場合には価値がありません。
ドーバル14年

はい、それはホワイトボックス単体テストの制限です。ただし、[1、sides](両端を含む)の範囲の値を正しく返すテストには意味がありません。random.randint()これは、randomユニットが正しく動作することを確認するPython開発者の責任です。
マーティンピーターズ14年

そして、あなた自身が言っているように、単体テストではコードにバグがないことを保証することはできません。コードが他のユニットを誤って使用している場合(たとえば、のrandom.randint()ように振る舞い、random.randrange()それを呼び出すと予想されるrandom.randint(1, sides + 1)場合、とにかく沈んでしまいます。
Martijn Pieters 14年

2
@MartijnPieters私はそこに同意しますが、それは私が反対していることではありません。random.randintが引数(1、sides)で呼び出されることをテストすることに反対しています。実装では、これが正しいことであると仮定しましたが、テストではその仮定を繰り返しています。その仮定が間違っていれば、テストは合格しますが、実装はまだ正しくありません。これは、書いて維持するのが非常に面倒な半ば証明です。
ドーバル14年

6

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

モッキングツールはクールですが、何かを実行できるからといって、それを実行する必要があるわけではありません。YAGNIは、機能と同様にテストフィクスチャにも適用されます。

モックされていない依存関係で簡単にテストできる場合は、ほとんど常にそうする必要があります。そうすることで、テスト数を増やすだけでなく、欠陥数を減らすことにテストを集中させることができます。過剰なモッキングは、誤解を招くカバレッジの数値を作成するリスクがあり、実際のテストを後の段階に延期する可能性があります。


3

あなたがDieそれについて考えるなら何ですか?-のラッパーにすぎませんrandom。これは、カプセル化random.randintし、アプリケーション独自の語彙の面で、それをラベルを再:Die.Roll

私は間の抽象化の別の層を挿入することは、関連見つからないDierandomのでDie、それ自体がすでに間接のこの層であり、アプリケーションとプラットフォーム間。

サイコロの缶詰の結果が必要な場合は、単にモックを作成しDie、モックを作成しないでくださいrandom

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

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


2

私が見る限り、乱数ジェネレーターをシードし、期待される結果を検証することは、有効なテストではありません。それはあなたのサイコロが内部でどのように動作するかについての仮定を行います。これはいたずらです。Pythonの開発者は、乱数ジェネレーターまたはダイを変更できます(注:「dice」は複数形、「die」は単数形です。クラスが1回の呼び出しで複数のダイロールを実装しない限り、おそらく「die」と呼ばれるべきです)別の乱数ジェネレーターを使用します。

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

私なら、たとえば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]でダイを作成することは受け入れられるはずです-そして、おそらく "blah"も。これは、4つの面があり、各面に文字があるダイである場合があります。マジックエイトボールのように、ゲーム「Boggle」が思い浮かびます。

そして最後に、これを熟考することをお勧めします:http : //lh6.ggpht.com/-fAGXwbJbYRM/UJA_31ACOLI/AAAAAAAAAPg/2FxOWzo96KE/s1600-h/random%25255B3%25255D.jpg


2

潮に逆らって泳ぐ危険性があるので、私はこれまで言及されていない方法を使用して、数年前にこの正確な問題を解決しました。

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

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

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


この質問では、RNGの存在を前提として、テストの内容を詳しく説明するのではなく、テストに使用するデータを正確に説明します。私の提案は、RNGをock笑するだけで徹底的にテストすることです。何をテストする価値があるかという質問は、質問で提供されていない情報に依存します。


RNGをモックして予測可能にしたとしましょう。それでは、何をテストしますか?質問は「次は有効/有用な単体テストでしょうか?」と尋ねます。0から5を返すようにモックするのはテストではなく、テストのセットアップです。どのように「それに応じて単体テスト」しますか?「バグをキャッチする」方法を理解できていません。「ユニット」テストに必要なものを理解するのに苦労しています。
bdrx 14

@bdrx:これは少し前のことです。今は違う答えをするでしょう。しかし、編集を参照してください。
david.pfx

1

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

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

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


-1

ダイロールの​​最も簡単なテストは、それを数十万回繰り返すことです。そして、考えられる各結果がおおよそ(1 /サイドの数)回ヒットしたことを検証します。6面ダイスの場合、可能性のある各値が約16.6%の時間でヒットするのがわかります。いずれかが1%以上ずれている場合は、問題があります。

この方法で回避すると、テストを変更せずに、簡単に、そして最も重要なことには、乱数を生成する基本的なメカニズムをリファクタリングできます。


1
この試験は、単に事前に定義された順序で両側一つずつをループすることを完全に非ランダム実装に渡す
GNAT

1
コーダーが(ダイにランダム化エージェントを使用せずに)悪意を持って何かを実装することを意図しており、単に「赤信号を緑にする」ために何かを見つけようとすると、ユニットテストで実際に解決できる以上の問題があります。
クリストファー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.