February 11, 2012

Использование виртуальных машин для автоматического тестировани

В системном программирования достаточно часто возникает ситуация, когда значительная часть функциональности программы перекладывается на внешние компоненты. Типичный пример - операции с 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:

Alexey Diyan said...

Очень интересная статья, спасибо что поделился информацией.

Скажи, а вы не рассматривали библиотеку 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 имеет смысл посмотреть.

Unknown said...

Текущая логика состоит из следующих шагов:

1) Запуск виртуальной машины

2) Копирование на нее кода проекта и тестов

3) Исполнение тестов

4) Копирование результатов и слияние их с результатами ри предыдущих тестов

5) Гашение vm и всяческая уборка за собой

execnet, как и другие системы удалённого исполнения, могут частично автоматизировать 3й и 4й пункты. Например там уже есть готовый код возврата удаленных(в смысле remote) исключений на хост, но они плохо совместимы с системами типа nosetestest + плагины, которые изначально писались без расчета на удаленное исполнение. Так, например, coverage хранит информацию о покрытии во внутренних структурах(или файлах) и обновляет ее по мере исполнения новых тестов - execnet не сможет корректно обработать такую ситуацию. В общем все системы RPC предполагают, что вернут на хост нужно только результат вызванной удаленно функции, что тут не подходит. Так что для данного варианта толку от них маловато будет :(

Но вот про xdist не читал, посмотрю, спасибо.