Ложный открытый (file_name) в модульных тестах
У меня есть исходный код, который открывает CSV-файл и устанавливает связь заголовка к значению. Исходный код приведен ниже:
def ParseCsvFile(source):
"""Parse the csv file.
Args:
source: file to be parsed
Returns: the list of dictionary entities; each dictionary contains
attribute to value mapping or its equivalent.
"""
global rack_file
rack_type_file = None
try:
rack_file = source
rack_type_file = open(rack_file) # Need to mock this line.
headers = rack_type_file.readline().split(',')
length = len(headers)
reader = csv.reader(rack_type_file, delimiter=',')
attributes_list=[] # list of dictionaries.
for line in reader:
# More process to happeng. Converting the rack name to sequence.
attributes_list.append(dict((headers[i],
line[i]) for i in range(length)))
return attributes_list
except IOError, (errno, strerror):
logging.error("I/O error(%s): %s" % (errno, strerror))
except IndexError, (errno, strerror):
logging.error('Index Error(%s), %s' %(errno, strerror))
finally:
rack_type_file.close()
Я пытаюсь высмеять следующее утверждение
rack_type_file = open(rack_file)
Как мне макетировать функцию open(...)?
8 ответов
По общему признанию, это старый вопрос, поэтому некоторые ответы устарели.
В текущей версии mock
В библиотеке есть удобная функция, предназначенная именно для этой цели. Вот как это работает:
>>> from mock import mock_open
>>> m = mock_open()
>>> with patch('__main__.open', m, create=True):
... with open('foo', 'w') as h:
... h.write('some stuff')
...
>>> m.mock_calls
[call('foo', 'w'),
call().__enter__(),
call().write('some stuff'),
call().__exit__(None, None, None)]
>>> m.assert_called_once_with('foo', 'w')
>>> handle = m()
>>> handle.write.assert_called_once_with('some stuff')
Документация здесь.
Чтобы смоделировать встроенную функцию, открытую с использованием mox __builtin__
модуль:
import __builtin__ # unlike __builtins__ this must be imported
m = mox.Mox()
m.StubOutWithMock(__builtin__, 'open')
open('ftphelp.yml', 'rb').AndReturn(StringIO("fake file content"))
m.ReplayAll()
# call the code you want to test that calls `open`
m.VerifyAll()
m.UnsetStubs()
Обратите внимание, что __builtins__
это не всегда модуль, он может быть типа dict, пожалуйста, используйте __builtin__
(без "s") модуль для ссылки на встроенные методы системы.
Больше о __builtin__
модуль: http://docs.python.org/library/__builtin__.html
Есть два способа, которые мне нравятся, в зависимости от ситуации.
Если ваш модульный тест будет вызывать ParseCsvFile напрямую, я бы добавил новый kwarg в ParseCsvFile:
def ParseCsvFile(source, open=open):
# ...
rack_type_file = open(rack_file) # Need to mock this line.
Затем ваш модульный тест может пройти другой open_func, чтобы выполнить макет.
Если ваш модульный тест вызывает какую-то другую функцию, которая, в свою очередь, вызывает ParseCsvFile, то обходить open_func только для тестов - это ужасно. В этом случае я бы использовал модуль макета. Это позволяет вам изменять функцию по имени и заменять ее объектом Mock.
# code.py
def open_func(name):
return open(name)
def ParseCsvFile(source):
# ...
rack_type_file = open_func(rack_file) # Need to mock this line.
# test.py
import unittest
import mock
from StringIO import StringIO
@mock.patch('code.open_func')
class ParseCsvTest(unittest.TestCase):
def test_parse(self, open_mock):
open_mock.return_value = StringIO("my,example,input")
# ...
Прост с декоратором (Python3):
def my_method():
with open(file="/1.txt", mode='r', encoding='utf-8') as file:
return file.read().strip()
@mock.patch("builtins.open", create=True)
def test_my_method(mock_open):
mock_open.side_effect = [
mock.mock_open(read_data="A").return_value
]
resA = my_method()
assert resA == "A"
mock_open.mock_calls == [mock.call(file="/1.txt", mode='r', encoding='utf-8')]
Я позволил себе переписать вашу примерную функцию:
Предположим, что ваша функция находится в файле с именем code.py
# code.py
import csv
import logging
def ParseCsvFile(source):
"""Parse the csv file.
Args:
source: file to be parsed
Returns: the list of dictionary entities; each dictionary contains
attribute to value mapping or its equivalent.
"""
global rack_file
rack_file = source
attributes_list = []
try:
rack_type_file = open(rack_file)
except IOError, (errno, strerror):
logging.error("I/O error(%s): %s", errno, strerror)
else:
reader = csv.DictReader(rack_type_file, delimiter=',')
attributes_list = [line for line in reader] # list of dictionaries
rack_type_file.close()
return attributes_list
Простой тестовый пример будет:
# your test file
import __builtin__
import unittest
import contextlib
from StringIO import StringIO
import mox
import code
@contextlib.contextmanager
def mox_replayer(mox_instance):
mox_instance.ReplayAll()
yield
mox_instance.VerifyAll()
class TestParseCSVFile(unittest.TestCase):
def setUp(self):
self.mox = mox.Mox()
def tearDown(self):
self.mox.UnsetStubs()
def test_parse_csv_file_returns_list_of_dicts(self):
TEST_FILE_NAME = 'foo.csv'
self.mox.StubOutWithMock(__builtin__, 'open')
open(TEST_FILE_NAME).AndReturn(StringIO("name,age\nfoo,13"))
with mox_replayer(self.mox):
result = code.ParseCsvFile(TEST_FILE_NAME)
self.assertEqual(result, [{'age': '13', 'name': 'foo'}]) # works!
if __name__ == '__main__':
unittest.main()
РЕДАКТИРОВАТЬ:
% /usr/bin/python2.6
Python 2.6.1 (r261:67515, Jun 24 2010, 21:47:49)
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import __builtin__
>>> import mox
>>> mock = mox.Mox()
>>> mock.StubOutWithMock(__builtin__, 'open')
>>> mock.UnsetStubs()
Работает нормально на 2.6 используя mox 0.53
Привет, у меня была похожая проблема, и я рвал на себе волосы, перебирая разные насмешливые библиотеки. Я наконец нашел решение, которым я доволен, и, может быть, оно может вам помочь? В конце я пошел с библиотекой Mocker http://labix.org/mocker и вот код для открытого макета:
from mocker import Mocker
from StringIO import StringIO
import __builtin__
mocker = Mocker()
sourceFile = 'myTestFile.txt'
__builtin__.open = mocker.mock()
__builtin__.open(sourceFile)
mocker.result(StringIO('this,is,a,test,file'))
<the rest of your test setup goes here>
mocker.replay()
ParseCsvFile(sourceFile)
mocker.restore()
mocker.verify()
Кстати, причина, по которой я пошел с Mocker, заключается в том, что я тестировал функцию, которая использовала open для чтения файла, а затем снова использовал open для перезаписи того же файла новыми данными. То, что мне нужно было сделать, это протестировать случай, когда первоначальный файл не существовал, поэтому создайте макет, который выдает IOError в первый раз, а затем работает во второй раз. Настройка для которой выглядела так:
from mocker import Mocker
import __builtin__
mocker = Mocker()
mockFileObject = mocker.mock()
__builtin__.open = mocker.mock()
__builtin__.open('previousState.pkl', 'r')
mocker.throw(IOError('Boom'))
__builtin__.open('previousState.pkl','w')
mocker.result(mockFileObject)
<rest of test setup >
mocker.replay()
<test>
mocker.restore() #required to restore the open method
mocker.verify()
Надеюсь это поможет!
@ mock.patch decorator (пример 2.7)
Теперь это намного проще:
import your_script.py
import __builtin__
import mock
@mock.patch("__builtin__.open")
def test_example(self, mock_open):
your_script.your_method()
self.assertEqual(mock_open.call_count, 1)
>>> class A(object):
... def __init__(self):
... self.x = open('test.py')
...
>>> old_open = open
>>> def open(s):
... return "test\n"
...
>>> a = A()
>>> a.x
'test\n'
>>> open = old_open
>>> a = A()
>>> a.x
<open file 'test.py', mode 'r' at 0xb7736230>