Pythonのmock_openで複数のファイルをopenするテストを書く方法のメモです。

mock_open | unittest.mock | docs.python
https://docs.python.org/ja/3/library/unittest.mock.html#mock-open

mock_open自体は、
次のように、open()をmockして単体テストを書く時に利用できます。
(ここでテスト対象としているtarget関数は、"/some/path"の内容を返却する関数)

from mock import patch, mock_open

def target():
    with open('/some/path', 'r') as f:
        return f.read()

def test_target():
    m = mock_open(read_data="something")
    with patch('builtins.open', m):
        assert target() == "something"

このエントリでは、
次のように複数のファイルをopenしてテストしたい場合の対応方法を考えます。

def target_multi():
    bodies = []
    with open('/some/path1', 'r') as f:
        bodies.append(f.read())
    with open('/some/path2', 'r') as f:
        bodies.append(f.read())
    return bodies

ところで、
m = mock_open(read_data="something")としてmockを作った場合、

  • mock_open … openのmockを生成するヘルパー関数
  • m … openのmock
  • m.return_value = m() … open関数の戻り値 = file handleのmock

という状態になっており、
openのmockが作られた時点でread_data(ファイルの内容)が決まってしまい、
openのmock呼び出し時のパラメータによって差し替えることができません。

そこで、
mock_openが作ってくれるfile handleのmockだけ利用して、
openのmockは、MagicMockを利用して自前で作ることにします。

from mock import patch, mock_open, MagicMock

def target_multi():
    bodies = []
    with open('/some/path1', 'r') as f:
        bodies.append(f.read())
    with open('/some/path2', 'r') as f:
        bodies.append(f.read())
    return bodies

def test_target_multi():
    read_data_map = {
        '/some/path1': 'something one',
        '/some/path2': 'something two',
    }
    mock = MagicMock(name='open', spec=open)
    mock.side_effect = lambda *args: mock_open(read_data=read_data_map[args[0]])()
    with patch('builtins.open', mock):
        assert target_multi() == ['something one', 'something two']

以上。