Эта статья написана по мотивам одноименного доклада на Highload++'2012. Предназначена она для руководителей, которые смогут, взглянув на наше тестирование, сравнить его с тестированием в своем проекте, для программистов и системных администраторов, которым представится возможность посмотреть на тестирование как на очень интересную работу, и, конечно, для тестировщиков.
В статье я расскажу, о том, каким на самом деле может быть тестирование, как мы сделали тестирование продуктивной и интересной работой, какие задачи мы решаем, и почему работать у нас хорошо.
Прежде всего, давайте разберемся, что такое тестирование. Тестирование – это процесс в разработке программного обеспечения, предоставляющий информацию о качестве. Все знают, что тестирование повышает качество продукта. Однако не все осознают, что происходит это за счет предоставления информации.
Тестирование: скучные задачи и интересные задачи
Что же нужно сделать, чтобы протестировать первую версию программы?
Сначала нам нужно будет настроить конфигурацию (установить и сконфигурировать операционную систему, все программы и библиотеки, которые требуются нашей программе). Потом нам необходимо будет установить и сконфигурировать саму тестируемую программу. После всего этого мы запустим программу и по каждому из требований, предъявляемых к ней заказчиком, передадим заранее подготовленные входные данные программе. Это можно сделать через пользовательский интерфейс, входные файлы, базу данных, по сети или иным способом, в зависимости от проверяемой функции. Далее мы дождемся окончания обработки, а затем получим каким-нибудь способом выходные данные и сравним их с эталоном. Это типичное общее описание тестирования. При ручном тестировании все эти действия выполняются инженерами вручную.
По мере выпуска следующих версий программы и добавления нового функционала нам нужно будет проверять с каждым разом все больше и больше функций. То есть, грубо говоря, трудозатраты будут линейно возрастать с ростом количества функций. И в какой-то момент руководитель окажется перед выбором: либо начать частично пропускать проверки старого функционала (регрессионное тестирование), либо постоянно увеличивать штат.
Ситуация может оказаться особенно тяжелой, если программу нужно тестировать в нескольких различных конфигурациях. В этом случае одни и те же действия придется повторять для каждой конфигурации.
Важно отметить, что только тестирование новых функций будет интересной задачей, а повторное тестирование старых функций (регрессионное тестирование) является монотонной однообразной работой, повторением уже не раз проделанных операций. Поэтому даже при постоянном расширении штата сотрудники будут демотивированы, что негативно скажется на внимательности и ответственности, а в итоге приведет к пропущенным ошибкам.
Также иногда заранее подготовленная конфигурация может быть случайно потеряна (например, из-за поломки тестового сервера). Тогда придется проводить этап настройки конфигурации заново, что тоже является повторением ранее проделанной работы и может занять много времени. Особенно тяжело, если действия по настройке конфигурации не были нигде зафиксированы.
При такой организации работы тестирование становится неэффективным, а работа в нем по-настоящему грустной и неинтересной.
Как мы решили эту проблему?
Избавление от скучной работы
Настройка конфигурации
Для организации тестовых стендов мы используем виртуальные машины Xen – таким образом, параллельные работы по тестированию не мешают друг другу, а мы имеем возможность быстро создавать новые стенды. Мы могли бы сделать для каждой тестовой конфигурации по эталонному образу диска виртуальной машины, но у такого подхода есть свои минусы. Во-первых, изменение таких образов требует манипуляций по монтированию их каким-то виртуальным машинам, что неудобно. Во-вторых, для создания эталонных образов для каждого стенда необходим большой дисковый объем. В-третьих, версионность при таком подходе можно организовать только через хранение целиком каждой версии образа для каждого стенда.
Чтобы решить все эти проблемы, мы используем систему автоматического конфигурирования Opscode Chef.
Chef хранит в своей базе «роли», т.е. набор скриптов по конфигурированию, и сами эти скрипты («рецепты»). Для каждого тестового стенда у нас есть «роль» и необходимые ей «рецепты». При создании виртуальной машины мы назначаем ей «роль», и при первом включении машина регистрируется на Chef-сервере, получает от него «рецепты» и выполняет их. Таким образом, мы можем хранить всего один эталонный образ для каждой операционной системы, необходимой в тестировании. Кроме того, благодаря хранению «рецептов» и «ролей» в git мы получаем версионность. «Рецепты» являются программами на Ruby, что дает возможность всегда четко понимать, какие именно действия выполняются для настройки стенда. Плюс ко всему, Chef дает возможность использовать готовые «рецепты», написанные сообществом.
Быстрое создание стенда
Для того чтобы можно было быстро выполнять создание новых виртуальных машин, назначение им ролей, а также их включение, выключение и удаление, мы настроили ConVirt Open Source – веб-интерфейс для управления виртуальными машинами. Мы выбрали именно его, потому что это бесплатное решение с открытым исходным кодом на Python, что дает нам возможность вносить в него исправления и добавлять новый функционал. Например, мы добавили возможность прописывать «роль» машины при ее создании.
Таким образом, создание машины производится с помощью всего одной веб-формы. В этой форме нужно выбрать эталонный образ диска в зависимости от того, какая операционная система нужна для данной машины, назначить уникальное имя и «роль» и нажать «ОК».
Итак, управление тестовыми стендами автоматизировано, и можно переходить к автоматизации самого тестирования.
Автоматизация тестов
Автоматизация тестирования необходима не только потому, что важно экономить ресурсы и избавляться от скучной и однообразной работы. Еще одна причина в том, что большая часть программ, проходящих тестирование, не имеет пользовательского интерфейса; это делает их ручное тестирование невозможным и вынуждает писать программы для взаимодействия с ними.
Как же мы автоматизируем тесты? Мы пишем полностью автоматизированные тесты (т.е. тесты, которые сами автоматически подготавливают программу к началу теста и формируют входные данные, выполняют тестовое действие и автоматически сравнивают результат с эталонным) на основе 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 возвращает -1. В процессе использования выяснилось, что, как правило, замена результата нужна не во всех случаях, а только в одном определенном, который можно опознать по значениям параметров вызова библиотечной функции. Чтобы это стало возможным, мы добавили в ltrace возможность делать умную подмену результата функции из языка Python.
Машина времени
Иногда нужно проверять, что программа выполняет какие-то действия в определенное время. Например, программа сбрасывает данные в базу в 3 часа ночи. Однако проверять эту функцию нужно для каждой версии программы независимо от текущего времени. Сразу же приходит в голову вариант переводить системное время на тестовом стенде, но такой способ повлияет на всю систему, включая сами тесты, а не только на тестируемую программу, что ведет к сложно отлаживаемым проблемам.
Раз уж мы научились подменять результат вызовов библиотечных функций с помощью ltrace, почему бы не добавить функционал, подменяющий результаты вызовов функций времени? В итоге мы добавили в ltrace функционал по подмене возвращаемых значений функций time, gettimeofdate, clock_gettime. Т.к. для нормальной работы тестируемой программе нужно, чтобы время шло вперед, у нас реализована эта возможность: время идет вперед относительно начального момента, заданного в параметрах ltrace.
Ltrace с функциями подмены результата вызовов и машины времени доступен на github по адресу https://github.com/zenovich/ltrace.
Проверка фильтра Блума
Еще одна из интересных задач, которые мы решали – проверка фильтра Блума.
Фильтр Блума – вероятностная структура данных, позволяющая компактно хранить множество элементов и проверять принадлежность заданного элемента к этому множеству. При этом существует возможность получить ложноположительное срабатывание (элемента в множестве нет, но структура данных сообщает, что он есть), но не ложноотрицательное. Фильтр Блума может использовать любой объём памяти, заранее заданный пользователем, причем чем он больше, тем меньше вероятность ложного срабатывания.
Обычно фильтр Блума используется для уменьшения числа запросов к несуществующим данным в структуре данных с более дорогостоящим доступом (например, расположенной на жестком диске или в сетевой базе данных), то есть для «фильтрации» запросов к ней.
Структура представляет собой битовый массив из m бит. Изначально, когда структура данных хранит пустое множество, все m бит обнулены. Пользователь должен определить k независимых хеш-функций h1, …, hk, отображающих каждый элемент в одну из m позиций битового массива достаточно равномерным образом.
Для добавления элемента e необходимо записать единицы на каждую из позиций h1(e), …, hk(e) битового массива.
Для проверки принадлежности элемента e к множеству хранимых элементов необходимо проверить состояние битов h1(e), …, hk(e). Если хотя бы один из них равен нулю, элемент не может принадлежать множеству. Если все они равны единице, то структура данных сообщает, что е принадлежит множеству. При этом могут возникнуть две ситуации: либо элемент действительно принадлежит множеству, либо все эти биты оказались установлены случайно при добавлении других элементов, что и является источником ложных срабатываний в этой структуре данных.
Вероятность ложноположительного срабатывания уменьшается с ростом m (размера битового массива), и увеличивается с ростом n (числа вставленных элементов). Для фиксированных m и n оптимальное число k (число хеш-функций), минимизирующих эту вероятность, равно (в предположении, что множество хеш-функций выбрано случайно, и для любого элемента x каждая хеш-функция hi назначает ему одно из мест в битовом массиве с равной вероятностью, а значения hi(x) являются независимыми в совокупности случайными величинами):
,
При этом сама вероятность ложного срабатывания равна
.
В реальности хеш-функции выбираются программистами, так что вероятность ложноположительного срабатывания может сильно отличаться от теоретической. Поэтому Блум-фильтры нужно тестировать.
Для проверки соответствия фильтра Блума требованиям мы заполняем его большим количеством тестовых данных (сотни миллионов элементов), имеющих такое же распределение, как и данные в продакшене, и сравниваем полученный процент коллизий (ложноположительных срабатываний) с процентом, прописанным в требованиях.
Проверка случайной выдачи
Еще одной интересной задачей является проверка случайной выдачи. Предположим, нам нужно проверить программу, которая в ответ на каждый запрос возвращает один из заранее заданных элементов, причем для каждого элемента задана вероятность, с которой он должен выдаваться. Это классическая задача математической статистики – проверка статистической гипотезы.
Для проверки такого функционала мы используем критерий Пирсона или χ2-критерий. Мы делаем N запросов, и для каждого элемента множества считаем, сколько раз он был возвращен (Oi). При этом нам известно, сколько раз каждый элемент должен был быть возвращен при идеальном выполнении гипотезы (Ei). По этим данным мы вычисляем величину
.
При выполнении гипотезы эта величина случайна (если программа выдает элементы случайным образом) и должна подчиняться распределению χ-квадрат. Таким образом, для заданного уровня значимости α наше значение χ2 должно быть больше квантили , где k – количество элементов множества. Если это не так, то выдача не случайна. С другой стороны, наше значение χ2 должно быть меньше квантили , иначе гипотеза не выполняется.
Распространено мнение, что для такой проверки нужно сделать очень много запросов к программе. На самом деле это не так. Минимальное количество запросов N для применения критерия
.
Т.е., например, если самый «редкий» элемент множества теоретически выдается с вероятностью 10%, нам нужно сделать всего 50 запросов.
Забавно, что такой метод проверки генераторов случайных чисел рекомендуется в книге Дональда Кнута, которую программисты часто используют в качестве подставки под монитор.
Рассмотрим простой пример. Предположим, что нам нужно проверить программу, выбирающую один из 4000 элементов с равной вероятностью: .
Для проверки нам нужно сделать N=5*4000=20 000 запросов к программе. По мере выполнения запросов мы сохраняем для каждого элемента количество выпадений в массив counted. Этот тест можно сделать очень быстрым, если реализовать его мультипроцессно.
Для языка Python есть прекрасная библиотека SciPy, с помощью которой очень просто вычислять значение χ2 и P-значение (т.е. вероятность того, что случайная величина с распределением χ2 примет значение не меньшее, чем фактическое) для него. Если гипотеза предполагает равномерное распределение значений, как в нашем простом случае, то χ2 и P-значение вычисляются с помощью такой строки кода на Python:
chi_square, p_value = scipy.stats.chisquare(counted)
Остается только проверить, что p_value лежит в диапазоне от 0.05 до 0.95 (для уровня значимости 5%). Забавно, что когда мы написали этот тест, P-значение оказалось на порядки меньше, чем 0.05. При этом отказ от мультипроцессности приводил результат к правильному. Оказалось, что в тестируемой программе, которая тоже работает в несколько процессов, в каждом процессе генератор случайных чисел инициализировался одним и тем же числом. После исправления программы мультипроцессный тест стал проходить успешно.
Заключение
В ходе нашей работы мы автоматизировали создание тестовых конфигураций, выполнение регрессионных тестов, анализ результатов тестов и публикацию отчетов. Так как теперь всю эту скучную и однообразную работу выполняют роботы, мы можем посвятить все время интересным задачам – созданию новых тестов, их автоматизации и разработке новых инструментов.
Таким образом, как вы видите, избавление от скучной и монотонной работы делает тестирование интересной и захватывающей работой, в которой есть место и программированию, и хакерству, и математике.
Автор: Дмитрий Зенович, руководитель тестирования Mail.Ru Group.
Автор: dzenovich