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

Ваши тесты не должны открывать файл напрямую, каждый тест должен скопировать файл и работать с его копией.