モックモジュール:テストでのダミーモックアップ

英語のモックとは、「模倣」、「偽物」を意味します。 この名前のモジュールは、Pythonでの単体テストを大幅に簡素化するのに役立ちます。



操作の原理は単純です。関数をテストする必要がある場合、それ自体に適用されないすべてのもの(たとえば、ディスクまたはネットワークからの読み取り)をダミーのモックアップに置き換えることができます。 同時に、テストされた関数はテストに適応する必要はありません。コードがパラメータとしてそれらを受け入れない場合でも、モックは他のモジュールのオブジェクトを置き換えます。 つまり、テストにまったく適応せずにテストできます。



この振る舞いは、もはやインフレータブルロケットランチャーではなく、テストロケットと飛行機が飛行できるインフレータブルランド全体です。











Habrでは、 Mockパッケージは1つのコメントでのみ言及されていました。 原則として、モンキーパッチと呼ばれる他のモジュールのオブジェクトを置き換える必要がある場合があります。 しかし、手動のパッチとは異なり、Mockは非常に複雑な置換、パック、および呼び出しのチェーンを実行できます。また、Mock自体をクリーンアップし、副作用を残しません。



パッケージは1メガバイト未満で、非常に簡単にインストールできます。



$ pip install mock





または

$ easy_install mock







そして今、それを使用することができます。



関数オーバーライド





私たちの機能が何かを考慮し、非常に長い間、としましょう:



 from itertools import permutations def real(name): if len(name) < 10: raise ValueError('String too short to calculate statistics.') y = 0 for i in permutations(xrange(len(name)), 10): y += i print y
      
      







この例は手に負えない原始的なものですが、同様のことが発生する可能性があります。大きな計算が内部で行われるため、テストでは繰り返したくありません。 この例では、短い行を入力したいので(置換の繰り返し回数len(name)が少なくなるように)、これは禁止されています。 関数を2つに分割し、順列を呼び出し、その出力を関数に渡すことは可能ですが、別の方法で行うこともできます。



コードを書き換える代わりに、呼び出し中に置換関数を「パッチ」し、特定の出力のみを設定してコードを呼び出すことができます。



 from mock import patch import itertools # :   ,    name = '  ' >>> with patch('itertools.permutations') as perm_mock: ... perm_mock.return_value = xrange(3) ... real(name) 1 3 6
      
      







注: 42の代わりにprintが呼び出されます /(42-10)! わずか3回、つまり、サイクルはxrange(3)を通り抜けました。



さらに、withコンテキストを終了した後、itertools.permutations関数は通常の状態に戻りました。



デバッグ情報





関数に渡したオブジェクトに何が起こるかを確認する必要があるとしましょう。 そのシーケンスで、それらのパラメーターを使用して、メソッドが呼び出されるかどうか、属性がアクセスされるかどうか。 Mockオブジェクトを実行するだけで、発生するすべてを記録できます。



生命からの同様の例:ブランの空力テストが行​​われたとき、船全体がパイプに吹き飛ばされず(何もありません)、大気中に放出されませんでした。 特別なプロトタイプBTS-002が空中を飛行し、改造されたTu-154が胴体キットに使用されて、着陸操作のためにブランの空気力学に追従しました。



-002



Mockオブジェクトには、呼び出し情報を持ついくつかの属性があります。







上記の例では、実()関数が順列を正しく呼び出すことを確認できます。 たとえば、自動テストでより正確に確認するには、 assert_ *メソッドのいずれかを呼び出すことができます。



 perm_mock.assert_called_with(xrange(len(name)), 10)
      
      







構文糖





Djangoの単体テストの場合、パッチはデコレーターとして機能します。



 @patch('itertools.permutations') def test(ip): ip.return_value = range(5) print list(itertools.permutations(xrange(10), 10)) >>> test() [0, 1, 2, 3, 4]
      
      







属性と呼び出しチェーンのモックアップ





Djangoでは、ファイルに対して何かを行う必要がある場合がありますが、ディスクやその他のストレージに保存せずに行う方が良い場合があります。 通常の方法では、Fileクラスから継承し、いくつかのプロパティを上書きしますが、これは面倒です。 しかし、Mockでは、属性と呼び出しチェーンの両方をすぐに記述することができます。



 mock_file = Mock() mock_file.name = 'my_filename.doc' mock_file.__iter__.return_value = [' 1', ' 2', ' 3'] stat = mock_file.stat.return_value stat.size, stat.access_time = 1000, datetime(2012, 1, 1)
      
      







以下に、保存されたテストコードの量を示します。 ところで、引数としてオブジェクトを渡すか、関数からオブジェクトを期待する場合、区別するために名前を付けると便利です:



 >>> mock_a = Mock(name=' ') >>> mock_a <Mock name=' ' id='169542476'>
      
      







これらの属性チェーンはどのように機能しますか?



 >>> m = Mock() >>> m <Mock id='167387660'> >>> m.any_attribute <Mock name='mock.any_attribute' id='167387436'> >>> m.any_attribute <Mock name='mock.any_attribute' id='167387436'> >>> m.another_attribute <Mock name='mock.another_attribute' id='167185324'>
      
      







ご覧のとおり、属性にアクセスするとMockクラスの別のインスタンスが返され、同じ属性に再度アクセスすると同じインスタンスが得られます。 属性には、関数を含むあらゆるものを指定できます。 最後に、任意のレイアウトを呼び出すことができます(たとえば、クラスの代わりに):



 >>> m() <Mock name='mock()' id='167186284'> >>> m() is m False
      
      







これは別のインスタンスになりますが、再度呼び出された場合、インスタンスは同じになります。 したがって、これらのオブジェクトにいくつかのプロパティを割り当ててから、このオブジェクトをテスト済みのコードに渡すと、そこに読み込まれます。



属性に値を割り当てた場合、驚くことはありません。次の呼び出しで、まさにこの値を取得します。



 >>> m.any_attribute <Mock name='mock.any_attribute' id='167387436'> >>> m.any_attribute = 5 >>> m.any_attribute 5
      
      







名前の衝突を避けるために、 クラスのドキュメントでクラスの属性を読むと便利です。



レイアウトの柔軟性を制限する方法





ご覧のとおり、AttributeErrorエラーを発生させることなく、任意のレイアウト属性にアクセスできます。 この便利さには裏返しがあります。たとえば、APIを変更した場合、メソッドの名前を変更すると、クラスで機能する関数は前のメソッドを参照しますか? コードは実際には機能せず、テストはエラーなしで実行されます。 これを行うには、 specパラメーターでオブジェクト仕様を指定できます(Mockクラスまたはパッチのいずれか)。存在しないプロパティにアクセスすると、レイアウトによってエラーが発生します。



 class Tanchik(object): model = 'T80' def shoot(self, target): print '!' def tank_sugar(target): print '%s ' % tank.model tank.shoot(target) return tank ================== import tanks @patch('tanks.Tanchik', spec=tanks.Tanchik) # <<==   spec def test_tank(tank_mock): assert isinstance(tank_sugar(tank_mock), tanks.Tanchik)
      
      







モデルの名前を変更するか、タンクを撮影するが、tank_sugarの修正を忘れると、テストは失敗します。



レイアウトをよりスマートにする方法





さて、Mockが必要なオブジェクトを不要なオブジェクトに置き換え、出力を置き換えることができるとしましょう。 関数を値(return_value)よりも複雑なものに置き換えることは可能ですか? 2つの方法があります。





 def simple_function(args): do_something_useful() with patch('module.BigHeavyClass', side_effect=simple_function) as mock_class: another_class.take(mock_class)
      
      







ところで、simple_functionにテスト出力を書き込む必要はありません。前述のように、mock_classオブジェクトのコードの最後でmethod_callsを読み取ることができるためです。



組み込み関数の置換





Mock自体は、言語に組み込まれた関数(len、iter)を置き換えることはできませんが、必要な「魔法」関数を使用してレイアウトを作成できます 。 たとえば、ここではファイルレイアウトを作成しています。



 >>> mock = Mock() >>> mock_bz2module.open.return_value.__enter__ = Mock(return_value='foo') >>> mock_bz2module.open.return_value.__exit__ = Mock(return_value=False) >>> with mock_bz2module.open('test.bz2') as m: ... assert m == 'foo'
      
      







そのような多くの場合、標準オブジェクト(リスト、ファイル、番号)をエミュレートする場合、テストに適した値のセットを持つMagicMockクラスがあります。



モックが機能しない場所





モジュールの作成者であるMichael Furd氏は、レイアウトが必要な場所とそうでない場所の原則は単純であると述べています。モックアップでテストする方が簡単な場合は、モックアップを使用する必要があります。



新しいモジュールと古いモジュールの2つのモジュールをテストする必要がある場合があります。また、多くのクロスコールがあります。 注意深く見る必要があります:モジュール全体の動作を徐々に書き直し始めたら、それは停止する時です-テストコードはモジュールコードと密接に接続されており、作業コードを変更するたびに、テストを変更する必要があります。 さらに、古いモジュールレイアウトの代わりにモジュールレイアウト全体を記述しようとしないでください。



私の個人的な経験では、Mockはデバッガーと競合する可能性があります。たとえば、 PuDBでは無限再帰が発生しました。 IPDBは正常に機能したため、 IPDBを使用してプロジェクトテストを実行し、PuDBのコードのみを実行しました。



結論





モックレイアウトは、いつでもどこでも置き換えることができます。 コードをテスト用に調整する必要はありません。つまり、開発が高速化され、場合によってはプロトタイピングも高速化されます。



テストから余分なものをすべて捨てることができ、すべてが時間とリソースを消費し、チェックする必要があるコードのみが機能するようになります。



ハード(スペック)が必要な場所、柔軟性が必要な場所、およびパッチが残らないレイアウト設定。



参照資料










All Articles