ファイル操作をmock化する

open とか codecs.open でファイルを開く場合は基本 with句で使っているので単純にopenをモックに書き換えようとすると相当面倒くさい。そのために mock_open という関数が用意されている。

open() をコンテキストマネージャーとして使う方法は、ファイルが必ず適切に閉じられるようにする素晴らしい方法で、今では一般的になっています

問題は、 open() をモックアウトしたところで、コンテキストマネージャーが使われる (__enter__() と __exit__() が呼ばれる) のはその 戻り値 だということです。
コンテキストマネージャーを MagicMock でモックするのは一般的かつ面倒なので、ヘルパー関数を用意しています。

https://docs.python.jp/3/library/unittest.mock.html#mock-open

具体的な使い方は以下の通り。 Read/Writeともに同じ open 関数を使っている限りは両方の呼び出しがmockされる。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import codecs
from unittest.mock import MagicMock, patch, mock_open   # python 2 の場合は from mock

from nose.tools import eq_, ok_

def readwrite():
    with codecs.open('read.txt', 'r') as fh:
        reads = fh.readlines()
    with codecs.open('write.txt', 'w') as fh:
        fh.write('line1\n')
        fh.write('line2\n')
        fh.write('line3\n')    
    return reads

def test_mock_open():
    mockio = mock_open(read_data='readline1\nreadline2\nreadline3\n')
    with patch('codecs.open', mockio):
        reads = readwrite()
        
    # 1. read mocked data
    eq_(reads, ['readline1\n', 'readline2\n', 'readline3\n'])
    
    # 2. how to check write contents
    fh = mockio()  # Get file handler mock object by calling mockio object
    actuals = [args[0] for (args, kwargs) in fh.write.call_args_list]
    eq_('line1\n', actuals[0])
    eq_('line2\n', actuals[1])
    eq_('line3\n', actuals[2])


if __name__ == '__main__':
    test_mock_open()