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

Тестирование: скучные задачи и интересные задачи
Что же нужно сделать, чтобы протестировать первую версию программы?


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

Также иногда заранее подготовленная конфигурация может быть случайно потеряна (например, из-за поломки тестового сервера). Тогда придется проводить этап настройки конфигурации заново, что тоже является повторением ранее проделанной работы и может занять много времени. Особенно тяжело, если действия по настройке конфигурации не были нигде зафиксированы.
При такой организации работы тестирование становится неэффективным, а работа в нем по-настоящему грустной и неинтересной.
Как мы решили эту проблему?
Избавление от скучной работы
Настройка конфигурации
Для организации тестовых стендов мы используем виртуальные машины Xen – таким образом, параллельные работы по тестированию не мешают друг другу, а мы имеем возможность быстро создавать новые стенды. Мы могли бы сделать для каждой тестовой конфигурации по эталонному образу диска виртуальной машины, но у такого подхода есть свои минусы. Во-первых, изменение таких образов требует манипуляций по монтированию их каким-то виртуальным машинам, что неудобно. Во-вторых, для создания эталонных образов для каждого стенда необходим большой дисковый объем. В-третьих, версионность при таком подходе можно организовать только через хранение целиком каждой версии образа для каждого стенда.
Чтобы решить все эти проблемы, мы используем систему автоматического конфигурирования Opscode Chef.

Быстрое создание стенда

Таким образом, создание машины производится с помощью всего одной веб-формы. В этой форме нужно выбрать эталонный образ диска в зависимости от того, какая операционная система нужна для данной машины, назначить уникальное имя и «роль» и нажать «ОК».
Итак, управление тестовыми стендами автоматизировано, и можно переходить к автоматизации самого тестирования.
Автоматизация тестов

Как же мы автоматизируем тесты? Мы пишем полностью автоматизированные тесты (т.е. тесты, которые сами автоматически подготавливают программу к началу теста и формируют входные данные, выполняют тестовое действие и автоматически сравнивают результат с эталонным) на основе UnitTest – стандартного для Python фреймворка юнит-тестирования. Важно отметить, что на основе этого фреймворка мы пишем не юнит-тесты, а функциональные тесты. Выбор UnitTest дает нам возможность использовать в тестах всю мощь языка Python и его библиотек. Также у нас появляется возможность использовать Python-библиотеки, разработанные нашими программистами. А написанные на C расширения языка Python позволяют использовать библиотеки наших С-программистов.
Автоматический запуск тестов
После автоматизации тестов остается автоматизировать еще и их запуск. Мы используем для этого систему непрерывной интеграции Jenkins. При появлении нового коммита в git-репозитории программы, либо при появлении нового дистрибутива программы в репозитории rpm-пакетов Jenkins создает новую виртуальную машину с нужной «ролью». Далее Jenkins дожидается ее настройки, компилирует программу (при изменении в git) или выкачивает нужные rpm-пакеты, устанавливает программу и тесты, запускает тесты и публикует результаты в веб-интерфейсе, рассылая уведомления в почту. После прогона тестов виртуальная машина удаляется. Для ускорения мы создали «пул виртуальных машин», в котором находятся заранее созданные машины для каждой тестируемой программы; в результате Jenkins забирает из пула уже готовую сконфигурированную машину.

Примеры интересных задач
Параллельный Selenium
Одна из интересных задач, которую мы решили, не является задачей по тестированию, поэтому рассказ о ней – это скорее лирическое отступление. Есть такой инструмент для автоматизации тестирования веб-интерфейсов – Selenium. Он позволяет автоматизированным тестам открывать окна браузеров, загружать в них тестируемые страницы, вводить текст в формы, кликать по элементам страниц, выполнять другие пользовательские действия и производить необходимые проверки. Мы тоже используем этот инструмент, хотя тестирование веб-интерфейсов – не самая большая часть нашей работы. Наши тесты веб-интерфейсов работают через Selenium 2 (webdriver), настроенный удаленно. У нас есть сервер Selenium-хаб, принимающий соединения от тестов, и несколько Selenium-нод, на которых установлены сами браузеры, и на которых реально выполняются все действия с веб-страницами.
Нам было важно, чтобы мы могли запускать различные тесты параллельно, и чтобы эти параллельно работающие тесты не мешали друг другу. Однако, к сожалению, Selenium в официальной поставке не всегда позволяет это делать. Особенно плохо работает параллельный запуск нескольких тестов в браузере IE или Opera на одной ноде.
Мы решили проблемы параллельной работы тестов на одной ноде, внеся исправления в Java и C++ код самого Selenium. Мы добавили блокировки на выполнение однопоточных действий, переключение фокуса окна перед теми действиями, которым оно необходимо. Также мы почининили многопоточность upload’а файлов в IE, и добавили эту функцию для Opera. На момент написания статьи все эти исправления работают с версией Selenium 2.26.
Предвосхищая возможный вопрос, хочу сказать, что мы очень хотим, чтобы наши исправления стали частью официального Selenium. Мы выкладывали наши патчи на github (например, в https://github.com/wladich/operadriver) и пересылали их разработчикам. Однако, в силу различных причин, ни один из патчей в полной мере пока еще не стал частью Selenium, хотя мы видим часть наших строк в коде последних версий Selenium. Самая свежая порция наших исправлений пока еще не открывалась, и мы будем рады, если у разработчиков Selenium есть интерес к ней.
Триггеры непредвиденных ситуаций

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

Раз уж мы научились подменять результат вызовов библиотечных функций с помощью ltrace, почему бы не добавить функционал, подменяющий результаты вызовов функций времени? В итоге мы добавили в ltrace функционал по подмене возвращаемых значений функций time, gettimeofdate, clock_gettime. Т.к. для нормальной работы тестируемой программе нужно, чтобы время шло вперед, у нас реализована эта возможность: время идет вперед относительно начального момента, заданного в параметрах ltrace.
Ltrace с функциями подмены результата вызовов и машины времени доступен на github по адресу
Проверка фильтра Блума
Еще одна из интересных задач, которые мы решали – проверка фильтра Блума.
Фильтр Блума – вероятностная структура данных, позволяющая компактно хранить множество элементов и проверять принадлежность заданного элемента к этому множеству. При этом существует возможность получить ложноположительное срабатывание (элемента в множестве нет, но структура данных сообщает, что он есть), но не ложноотрицательное. Фильтр Блума может использовать любой объём памяти, заранее заданный пользователем, причем чем он больше, тем меньше вероятность ложного срабатывания.
Обычно фильтр Блума используется для уменьшения числа запросов к несуществующим данным в структуре данных с более дорогостоящим доступом (например, расположенной на жестком диске или в сетевой базе данных), то есть для «фильтрации» запросов к ней.

Для добавления элемента e необходимо записать единицы на каждую из позиций
Для проверки принадлежности элемента e к множеству хранимых элементов необходимо проверить состояние битов
Вероятность ложноположительного срабатывания уменьшается с ростом m (размера битового массива), и увеличивается с ростом n (числа вставленных элементов). Для фиксированных m и n оптимальное число k (число хеш-функций), минимизирующих эту вероятность, равно (в предположении, что множество хеш-функций выбрано случайно, и для любого элемента x каждая хеш-функция hi назначает ему одно из мест в битовом массиве с равной вероятностью, а значения hi(x) являются независимыми в совокупности случайными величинами):

При этом сама вероятность ложного срабатывания равна

В реальности хеш-функции выбираются программистами, так что вероятность ложноположительного срабатывания может сильно отличаться от теоретической. Поэтому Блум-фильтры нужно тестировать.
Для проверки соответствия фильтра Блума требованиям мы заполняем его большим количеством тестовых данных (сотни миллионов элементов), имеющих такое же распределение, как и данные в продакшене, и сравниваем полученный процент коллизий (ложноположительных срабатываний) с процентом, прописанным в требованиях.
Проверка случайной выдачи

Для проверки такого функционала мы используем критерий Пирсона или

При выполнении гипотезы эта величина случайна (если программа выдает элементы случайным образом) и должна подчиняться распределению χ-квадрат. Таким образом, для заданного уровня значимости α наше значение χ2 должно быть больше квантили


Распространено мнение, что для такой проверки нужно сделать очень много запросов к программе. На самом деле это не так. Минимальное количество запросов N для применения критерия

Т.е., например, если самый «редкий» элемент множества теоретически выдается с вероятностью 10%, нам нужно сделать всего 50 запросов.

Рассмотрим простой пример. Предположим, что нам нужно проверить программу, выбирающую один из 4000 элементов с равной вероятностью:

Для проверки нам нужно сделать N=5*4000=20 000 запросов к программе. По мере выполнения запросов мы сохраняем для каждого элемента количество выпадений в массив counted. Этот тест можно сделать очень быстрым, если реализовать его мультипроцессно.
Для языка Python есть прекрасная библиотека SciPy, с помощью которой очень просто вычислять значение χ2 и
chi_square, p_value = scipy.stats.chisquare(counted)
Остается только проверить, что p_value лежит в диапазоне от 0.05 до 0.95 (для уровня значимости 5%). Забавно, что когда мы написали этот тест, P-значение оказалось на порядки меньше, чем 0.05. При этом отказ от мультипроцессности приводил результат к правильному. Оказалось, что в тестируемой программе, которая тоже работает в несколько процессов, в каждом процессе генератор случайных чисел инициализировался одним и тем же числом. После исправления программы мультипроцессный тест стал проходить успешно.
Заключение
В ходе нашей работы мы автоматизировали создание тестовых конфигураций, выполнение регрессионных тестов, анализ результатов тестов и публикацию отчетов. Так как теперь всю эту скучную и однообразную работу выполняют роботы, мы можем посвятить все время интересным задачам – созданию новых тестов, их автоматизации и разработке новых инструментов.

Таким образом, как вы видите, избавление от скучной и монотонной работы делает тестирование интересной и захватывающей работой, в которой есть место и программированию, и хакерству, и математике.
Автор: Дмитрий Зенович, руководитель тестирования Mail.Ru Group.