В системном программирования достаточно часто возникает ситуация, когда значительная часть функциональности программы перекладывается на внешние компоненты. Типичный пример - операции с iptables, дисковыми образами и виртуальными машинами. Классически для тестирования такого кода используются массовые моки, отрезающие тестируемый код от всех внешних зависимостей.
При очевидных достоинствах (полная независимость тестов от внешнего мира, скорость исполнения, etc) у моков есть некоторое количество недостатков - самый главный это переход от тестирования того что должно быть сделано к тестирования того как это сделано. Если нужно проверить функцию, которая настраивает проброс порта, то вместо тестирования результата (правильного прохождения пакетов) проверяется, что iptables вызвалась с правильными параметрами.
По итогу юнит тест проверяет не правильность работы кода, а является отражением его структуры. Такой тест помогает обеспечить постоянную проверку на отсутствие AttributeError и ему подобных (python), но на этом его полезность оканчивается. Учитывая желание менеджера и/или заказчика получить заветные X% покрытия ситуация становится совсем идиотской. Несколько последних проектов, в которых я учавствовал были именно такие - тонкая прослойка из python, связывающая вместе БД, REST, xen, iptables и еще горстку linux утилит в небольшой специализированный клауд. По итогу заметная часть UT требует переписывания после каждого рефакторинга, потому как изменилось взаимодействие с внешними компонентами. То что должно поощрять рефакторинг и улучшение кода становится одним из главных его тормозов.
Частично эта ситуация отражает объективный факт - мы не можем позволить юнит-тестам бесконтрольтно модифицировать файловую систему на локальной машине, изменять правила прохождения пакетов или ip маршруты. Дополнительный минус - рабочая машина разработчика не всегда соответствует требованиям к конечному серверу.
Решение совершенно очевидное - использовать для тестов виртуальные машины и проводить тесты на необходимой конфигурации + исключить бОльшую часть моков из тестов.
Итого - что хотелось получить:
- исполнять отдельные юнит-тесты на виртуальных машинах или группах машин
- интеграция с nosetests и coverage
- максимально простое использование
- высокая скорость - юнит-тесты должны исполняться быстро
Как хотелось это использовать:
Без подсветки синтаксиса@on_vm('worker-1')
def test_iptables():
make_iptables_rules()
check_packages_goes_ok()
@on_vm('worker-2')
def test_something():
make_something()
check_something_works()
Доводить идею до рабочего варианта в рамках внутреннего проекта взялись интерны нашей компании - Игорь Гарагатый и Настя Криштопа.
Для начала было решено реализовать достаточно простой вариант: перед исполнением каждого теста, требующего виртуальную машину, запускалась соответствующая vm, на нее копировался код и тесты, запускались тесты и их результаты тестов возвращались назад на хост машину. Если тест выбросит исключение оно должно передаваться назад на хост и выбрасываться из локального теста - nose не должен замечать разницы между локальным и удаленным исполнением теста.
В итоге были выбраны два варианта - LXC и KVM. LXC позволяет запустить виртуальную машину менее чем за секунду и не требует аппаратной поддержки виртуализации, а KVM это более надежный вариант, позволяющий запускать виртуальные машины любых конфигураций (LXC использует ядро хост системы, поэтому поднять в нем другую версию ядра или другую OS невозможно).
Хотелось иметь в vm файловую систему доступную для записи, но возвращаемую в начальное состояние после окончания теста. Для kvm это естественным образом решается возможностями qcow2, который позволяет сохранять все изменения в отдельный файл, не трогая оригинальный образ. Для LXC же нужна была файловая система с поддержкой снимков и быстрым откатом к ним. После рассмотрения btrfs, LVM+XFS и aufs решили остановиться на первом варианте.
Что в итоге получилось:
- Пользователь подготавливает образы и конфигурации виртуальных машин, которые будут использоваться для UT
- Оборачивает отдельные тесты декоратором on_vm с указанием на какой конфигурации его запускать
- nosetests unitTests
- profit (итоги тестов и coverage)
Примерная схема работы:
- Декоратор on_vm создает отдельный процесс, для поднятия ВМ и запускает поток, слушающий результаты на сокете
- test_executor.py создает с помощью libvirt необходимую конфигурацию vm, предварительно сделав слепок btrfs или подключив qcow2 файл для сохранения изменений (в зависимости от типа виртуальной машины)
- test_executor.py дожидается окончания запуска vm, копирует туда необходимые файлы и запускает только выбранные тест на исполнение, предварительно выставив переменные окружения
- on_vm по переменным окружения определяет, что это реальных запуск и исполняет тест
- при возникновении ошибки она сериализуется и передается на хост
- итоги теста передаются на хост вместе с результатами покрытия
- процесс на хосте принимает результаты, гасит vm, откатывает состояние образа и имитирует локальное исполнение теста.
На текущий момент результат пока в состоянии альфа готовности, еще много чего хотелось бы добавить (иммитацию правильного времени исполнения, повторное использование уже запущенных vm, поднятие групп vm с определенными сетевыми настройками), но текущая реализация уже готова для проб. Код можно найти тут vm_ut.
Ссылки:github.com/koder-ua/vm_ut
koder-ua.blogspot.com/2012/01/lxc.html
mirantis.com
github.com/ogirOK
github.com/anakriya
pypi.python.org/pypi/coverage
readthedocs.org/docs/nose/en/latest
Исходники этого и других постов со скриптами лежат тут - github.com/koder-ua. При использовании их, пожалуйста, ссылайтесь на koder-ua.blogspot.com.
2 comments:
Очень интересная статья, спасибо что поделился информацией.
Скажи, а вы не рассматривали библиотеку execnet?
Features — execnet v1.0.9 documentation
http://codespeak.net/execnet/
Эта библиотека является основой в плагине xdist для фреймверка pytest.
xdist: pytest distributed testing plugin
http://pytest.org/latest/xdist.html#xdist
Хотя xdist решает несколько иную задачу. Там стоит вопрос о тривиальном распределенном тестировании, когда тесты выполняются на идентичных хостах.
Судя по вашей задаче, речь идет о наборе виртуальных машин, каждая из которых имеет специфическую конфигурацию.
В любом случае, думаю что на xdist имеет смысл посмотреть.
Текущая логика состоит из следующих шагов:
1) Запуск виртуальной машины
2) Копирование на нее кода проекта и тестов
3) Исполнение тестов
4) Копирование результатов и слияние их с результатами ри предыдущих тестов
5) Гашение vm и всяческая уборка за собой
execnet, как и другие системы удалённого исполнения, могут частично автоматизировать 3й и 4й пункты. Например там уже есть готовый код возврата удаленных(в смысле remote) исключений на хост, но они плохо совместимы с системами типа nosetestest + плагины, которые изначально писались без расчета на удаленное исполнение. Так, например, coverage хранит информацию о покрытии во внутренних структурах(или файлах) и обновляет ее по мере исполнения новых тестов - execnet не сможет корректно обработать такую ситуацию. В общем все системы RPC предполагают, что вернут на хост нужно только результат вызванной удаленно функции, что тут не подходит. Так что для данного варианта толку от них маловато будет :(
Но вот про xdist не читал, посмотрю, спасибо.
Post a Comment