Создание модульного теста Python, который никогда не выполняется параллельно

tl;dr — я хочу написать функцию Python unittest, которая удаляет файл, запускает тест и восстанавливает файл. Это вызывает состояние гонки, потому что unittest запускает несколько тестов параллельно, а удаление и создание файла для одного теста портит другие тесты, которые выполняются в то же время.

Длинный конкретный пример:

У меня есть модуль Python с именем converter.py, и с ним связаны тесты в test_converter.py. Если в том же каталоге, что и converter.py, есть файл с именем config_custom.csv, будет использоваться пользовательская конфигурация. Если нет пользовательского файла конфигурации CSV, то в converter.py встроена конфигурация по умолчанию.

Я написал модульный тест, используя unittest из стандартной библиотеки Python 2.7, чтобы проверить это поведение. Модульный тест в setUp() переименует config_custom.csv в wrong_name.csv, затем запустит тесты (надеюсь, используя конфигурацию по умолчанию), затем в tearDown() переименует файл обратно так, как он должен быть.

Проблема. Модульные тесты Python выполняются параллельно, и я столкнулся с ужасными условиями гонки. Файл config_custom.csv будет переименован посреди других модульных тестов недетерминированным образом. Это вызовет по крайней мере одну ошибку или сбой примерно в 90% случаев, когда я запускал весь набор тестов.

Идеальным решением было бы сказать unittest: НЕ запускайте этот тест параллельно с другими тестами, этот тест особенный и требует полной изоляции.

Мой обходной путь — добавить необязательный аргумент в функцию, которая ищет файлы конфигурации. Аргумент передается только набором тестов. Он игнорирует файл конфигурации, не удаляя его. На самом деле удаление тестового файла более изящно, это то, что я действительно хочу проверить.


person SerMetAla    schedule 14.02.2014    source источник


Ответы (4)


Мне часто нужно запускать тесты с полной изоляцией. Единственный способ, который, как я обнаружил, работает последовательно, — это поместить эти тесты в отдельные классы. Согласен, что обработка конфигурационных файлов внутри тестов по-прежнему доставляет неудобства.

Для чего-то, что я делаю прямо сейчас, я также могу попробовать что-то вроде pytest-ordering для проведения тестов более детерминированным образом.

person szeitlin    schedule 05.07.2018

Во-первых, большинство сред тестирования, поддерживающих параллелизм, также поддерживают методы для последовательного выполнения определенных важных тестов. Например, Python Fabric поддерживает последовательную аннотацию (когда вы запускаете свой тестировать как параллельный пакет с флагом командной строки -P):

from fabric.api import *

def runs_in_parallel():
    pass

@serial
def runs_serially():
    pass

Тем не менее, я не думаю, что вы должны делать это, я думаю, что вы должны остановиться и презреть на мгновение. Следуя принципу единой ответственности, я бы сказал, что это происходит.

Вы находитесь в ситуации, когда тест, по сути, определил состояние вашего кода, он выделяет для вас небольшое пятно единства.

Что вам нужно сделать сейчас, так это понять, что у «converter.py» слишком много обязанностей, он не только выполняет некоторые сложные операции под капотом, но также несет ответственность за управление своим собственным созданием и настройкой, что, вероятно, слишком много. для этого.

Возможно, если бы у вас был объект, который может управлять чтением в этом файле, вы могли бы легко имитировать/заглушить его и создать описываемое вами условие, которое вы хотите протестировать.

person MattSaw    schedule 14.02.2014
comment
Я знаю о Fabric, но не знаю декоратора @serial, так что спасибо. Относительно принципа единственной ответственности: я чувствую, что уже хорошо его придерживаюсь. converter.py — это всего лишь один файл, но он имеет несколько функций (не слишком много). Одна из функций — load_config(), и у нее всего одна обязанность. Если он находит файл конфигурации, загружает его, в противном случае использует значения по умолчанию. На самом деле ему не нужны никакие аргументы, но я добавил load_config(use_defaults=False) и передал True ТОЛЬКО во время модульных тестов, что вы и предложили. Это неоптимально. - person SerMetAla; 08.07.2018
comment
(Часть 2 из 2) Использование такого kwarg имеет две проблемы: во-первых, исходный код converter.py стал более сложным. Я только что добавил в converter.py функцию, которую никто никогда не должен использовать, ее должны использовать только модульные тесты. Это определение того, что относится к тестам, а не к самому коду. Во-вторых: что, если все тесты проходят с использованием этой подделки, но на самом деле отсутствие файла приводит к серьезному сбою? Модульные тесты больше не видят эту проблему. Модульные тесты пройдут. Если os.listdir() изменится и будет регресс, я его не увижу. - person SerMetAla; 08.07.2018
comment
Да, я бы согласился с вашими пунктами там. Ответил на этот вопрос давным-давно и имел бы разные представления о вещах на данный момент. - person MattSaw; 08.07.2018

Лучшей стратегией тестирования было бы убедиться, что вы тестируете непересекающиеся наборы данных. Это позволит обойти любые условия гонки и упростить код. Я бы также издевался над open или __enter__/__exit__, если вы используете менеджер контекста. Это позволит вам подделать событие, что файл не существует.

person Dan    schedule 07.12.2015

Проблема в том, что имя config_custom.csv само по себе должно быть настраиваемым параметром. Затем каждый тест может просто искать config_custom_<nonce>.csv, и любое количество тестов может выполняться параллельно.

Очистка всего набора может просто очистить config_custom_*.csv, так как в этот момент они нам не понадобятся.

person U2EF1    schedule 14.02.2014
comment
Имя файла на самом деле настраивается. Я действительно хочу проверить поведение, если ничего не найдено. Мой обходной путь, упомянутый в моем первоначальном вопросе, в основном похож на это предложение, и на самом деле он не проверяет поведение, которое я хочу. Моя работа заключается в том, чтобы сказать поисковику config-file-finder не искать какие-либо файлы конфигурации. Вы предлагаете в основном искать только определенные файлы конфигурации. Я хочу проверить поведение, если оно выглядит нормально и ничего не находит. Я не хочу отключать или ограничивать сам поиск. - person SerMetAla; 08.07.2018
comment
@SerMetAla Использование отдельных файлов - хороший способ сделать это без блокировки. Если вы не можете использовать отдельные наборы файлов, вам следует просто использовать блокировки. - person U2EF1; 10.07.2018