Pythonはどのようにユニットテストで対象にパッチを適用しますか?


問題
あなたが書いたセルテストでは、指定されたオブジェクトにパッチを適用して、それらのテスト中の所望の行為(例えば、呼び出し時のパラメータの個数を言い切る、指定された属性にアクセスするなど)を言い切る必要があります。
ソリューションunittest.mock.patch() 関数はこの問題を解決するために使用されてもよい。patch() はまた、一般的ではないが、装飾器、コンテキストマネージャ、または別個の使用としても使用されてもよい。例えば、以下は飾り器として使用する例である。

from unittest.mock import patch
import example

@patch('example.func')
def test1(x, mock_func):
  example.func(x)    # Uses patched example.func
  mock_func.assert_called_with(x)
コンテキストマネージャとしても使用できます。

with patch('example.func') as mock_func:
  example.func(x)   # Uses patched example.func
  mock_func.assert_called_with(x)
最後に、手動でパッチを使うこともできます。

p = patch('example.func')
mock_func = p.start()
example.func(x)
mock_func.assert_called_with(x)
p.stop()
できれば、装飾器とコンテキストマネージャを重畳して、複数のオブジェクトにパッチをかけることができます。たとえば:

@patch('example.func1')
@patch('example.func2')
@patch('example.func3')
def test1(mock1, mock2, mock3):
  ...

def test2():
  with patch('example.patch1') as mock1, \
     patch('example.patch2') as mock2, \
     patch('example.patch3') as mock3:
  ...
討論するpatch() は、既存のオブジェクトの全パス名を受け入れ、新しい値に置き換えます。元の値は装飾器関数またはコンテキストマネージャが完了したら自動的に元に戻ります。デフォルトの場合、すべての値はMagicMockの例によって置き換えられます。たとえば:

>>> x = 42
>>> with patch('__main__.x'):
...   print(x)
...
<MagicMock name='x' id='4314230032'>
>>> x
42
>>>
しかし、あなたはpatch() に第二のパラメータを提供することによって、値を任意のあなたが望むものに置き換えることができます。

>>> x
42
>>> with patch('__main__.x', 'patched_value'):
...   print(x)
...
patched_value
>>> x
42
>>>
代替値として使用されるMagicMockの例は、呼び出し可能なオブジェクトおよびインスタンスをシミュレーションすることができる。彼らは対象の使用情報を記録し、判定検査を行うことを許可します。例えば、

>>> from unittest.mock import MagicMock
>>> m = MagicMock(return_value = 10)
>>> m(1, 2, debug=True)
10
>>> m.assert_called_with(1, 2, debug=True)
>>> m.assert_called_with(1, 2)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File ".../unittest/mock.py", line 726, in assert_called_with
  raise AssertionError(msg)
AssertionError: Expected call: mock(1, 2)
Actual call: mock(1, 2, debug=True)
>>>

>>> m.upper.return_value = 'HELLO'
>>> m.upper('hello')
'HELLO'
>>> assert m.upper.called

>>> m.split.return_value = ['hello', 'world']
>>> m.split('hello world')
['hello', 'world']
>>> m.split.assert_called_with('hello world')
>>>

>>> m['blah']
<MagicMock name='mock.__getitem__()' id='4314412048'>
>>> m.__getitem__.called
True
>>> m.__getitem__.assert_called_with('blah')
>>>
一般的には、これらの操作は一つのユニットテストで完了します。例えば、下記のような関数が既にあったとします。

# example.py
from urllib.request import urlopen
import csv

def dowprices():
  u = urlopen('http://finance.yahoo.com/d/quotes.csv?s=@^DJI&f=sl1')
  lines = (line.decode('utf-8') for line in u)
  rows = (row for row in csv.reader(lines) if len(row) == 2)
  prices = { name:float(price) for name, price in rows }
  return prices
正常には、この関数はurlopen() を使用してWeb上からデータを取得して解析します。ユニットテストでは、事前に定義されたデータセットを与えることができます。パッチを使った操作の例を以下に示します。

import unittest
from unittest.mock import patch
import io
import example

sample_data = io.BytesIO(b'''\
"IBM",91.1\r
"AA",13.25\r
"MSFT",27.72\r
\r
''')

class Tests(unittest.TestCase):
  @patch('example.urlopen', return_value=sample_data)
  def test_dowprices(self, mock_urlopen):
    p = example.dowprices()
    self.assertTrue(mock_urlopen.called)
    self.assertEqual(p,
             {'IBM': 91.1,
             'AA': 13.25,
             'MSFT' : 27.72})

if __name__ == '__main__':
  unittest.main()
この例では、exampleモジュールに位置するurlopen() 関数は、テストデータを含むByteIO()を返すアナログオブジェクトに置き換えられる。
もう一つは、パッチを適用する時にexample.urlopen を使ってurllib.request.urlopen の代わりにしました。パッチを作成するときは、テストコードの名前を使わなければなりません。テストコードはfrom urllib.request import urlopen を使用するので、dowprices() 関数で使用されるurlopen() 関数は実際にexampleモジュールに位置します。
この節は実際にはunittest.mock モジュールについての簡単な試みだけです。もっと高級な特性は公式文書を参照してください。
以上はPythonがどのようにユニットテストで対象にパッチをかけるかの詳細です。Pythonユニットテストに関する資料は他の関連記事に注目してください。