Python unit test, который использует внешний файл данных
У меня есть проект Python, над которым я работаю в Eclipse, и у меня есть следующая файловая структура:
/Project
/projectname
module1.py
module2.py
# etc.
/test
testModule1.py
# etc.
testdata.csv
В одном из моих тестов я создаю экземпляр одного из моих классов, предоставляя 'testdata.csv'
в качестве параметра. Этот объект выполняет open('testdata.csv')
и считывает содержимое.
Если я запускаю только один тестовый файл с unittest
, все работает, и файл будет найден и правильно прочитан. Однако, если я попытаюсь выполнить все мои модульные тесты (например, запустив правой кнопкой мыши каталог test
, а не отдельный тестовый файл), я получаю сообщение об ошибке, что файл не найден.
Есть ли способ обойти это (кроме предоставления абсолютного пути, который я бы предпочел не делать)?
Ответы
Ответ 1
Обычно я определяю
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
вверху каждого тестового модуля. Тогда не имеет значения, в какой рабочей директории вы находитесь: путь к файлу всегда совпадает с местом, где находится тестовый модуль.
Затем я использую что-то вроде этого в своем тесте (или тестовой настройке):
my_data_path = os.path.join(THIS_DIR, os.pardir, 'data_folder/data.csv')
Или в вашем случае, поскольку источник данных находится в тестовом каталоге:
my_data_path = os.path.join(THIS_DIR, 'testdata.csv')
Ответ 2
Unit test, что доступ к файловой системе, как правило, не очень хорошая идея. Это связано с тем, что тест должен быть самодостаточным, делая ваши тестовые данные внешними по отношению к тесту уже не сразу очевидными, какой тест принадлежит файлу csv или даже если он все еще используется.
Предпочтительным решением является патч open
и заставить его возвращать файл-подобный объект.
from unittest import TestCase
from unittest.mock import patch, mock_open
from textwrap import dedent
class OpenTest(TestCase):
DATA = dedent("""
a,b,c
x,y,z
""").strip()
@patch("builtins.open", mock_open(read_data=DATA))
def test_open(self):
# Due to how the patching is done, any module accessing `open' for the
# duration of this test get access to a mock instead (not just the test
# module).
with open("filename", "r") as f:
result = f.read()
open.assert_called_once_with("filename", "r")
self.assertEqual(self.DATA, result)
self.assertEqual("a,b,c\nx,y,z", result)
Ответ 3
На мой взгляд, лучший способ справиться с этими случаями - это программировать с помощью инверсии управления.
В двух разделах ниже я прежде всего покажу, как будет выглядеть решение без инверсии управления. Во втором разделе показано решение с инверсией управления и как этот код может быть протестирован без макета.
В конце я заявляю некоторые личные плюсы и минусы, которые вовсе не имеют намерения быть правильными или полными. Не стесняйтесь комментировать для дополнения и исправления.
Нет инверсии управления (нет внедрения зависимости)
У вас есть класс, который использует open
метод std из python.
class UsesOpen(object):
def some_method(self, path):
with open(path) as f:
process(f)
# how the class is being used in the open
def main():
uses_open = UsesOpen()
uses_open.some_method('/my/path')
Здесь я явно использовал open
в своем коде, поэтому единственный способ написать для него тесты - это использовать явные данные (файлы) test или использовать mocking-framework, как предлагает Dunes. Но есть еще один способ:
Мое предложение: инверсия контроля (с внедрением зависимости)
Сейчас я переписал класс по-другому:
class UsesOpen(object):
def __init__(self, myopen):
self.__open = myopen
def some_method(self, path):
with self.__open(path) as f:
process(f)
# how the class is being used in the open
def main():
uses_open = UsesOpen(open)
uses_open.some_method('/my/path')
Во втором примере я ввел зависимость для open
в конструктор (Constructor Dependency Injection).
Написание тестов для инверсии управления
Теперь я могу легко писать тесты и использовать мою тестовую версию open
когда мне это нужно:
EXAMPLE_CONTENT = """my file content
as an example
this can be anything"""
TEST_FILES = {
'/my/long/fake/path/to/a/file.conf': EXAMPLE_CONTENT
}
class MockFile(object):
def __init__(self, content):
self.__content = content
def read(self):
return self.__content
def __enter__(self):
return self
def __exit__(self, type, value, tb):
pass
class MockFileOpener(object):
def __init__(self, test_files):
self.__test_files = test_files
def open(self, path, *args, **kwargs):
return MockFile(self.__test_files[path])
class TestUsesOpen(object):
def test_some_method(self):
test_opener = MockFileOpener(TEST_FILES)
uses_open = UsesOpen(test_opener.open)
# assert that uses_open.some_method('/my/long/fake/path/to/a/file.conf')
# does the right thing
Pro/Con
Pro Dependency Injection
- не нужно учить насмешливые рамки для тестов
- полный контроль над классами и методами, которые должны быть подделаны
- также изменение и развитие вашего кода легче в целом
- Качество кода обычно улучшается, так как одним из наиболее важных факторов является способность реагировать на изменения настолько просто, насколько это возможно
- использование внедрения зависимостей и среды внедрения зависимостей - это, как правило, уважаемый способ работы над проектом https://en.wikipedia.org/wiki/Dependency_injection
Con Dependency Injection
- немного больше кода, чтобы написать в целом
- в тестах не так мало, как исправление класса через @patch
- Конструкторы могут быть перегружены зависимостями
- нужно как-то научиться пользоваться зависимостями-инъекциями
Ответ 4
Ваши тесты не должны открывать файл напрямую, каждый тест должен скопировать файл и работать с его копией.