- PVSM.RU - https://www.pvsm.ru -
Обсуждая тестирование, чаще всего спикеры говорят об особенностях подхода, известного как «черный ящик». Но здесь мы поговорим о противоположном сценарии — «белом ящике», позволяющем формулировать вопросы к коду, понимая его внутреннюю структуру.
В основе статьи — расшифровка доклада Никиты Макарова (Одноклассники) с нашей декабрьской конференции Heisenbug 2017 Moscow.
На большом количестве конференций и в очень большом количестве книг, блог-постов и прочих источников говорится о том, что тестирование методом черного ящика — это хорошо и правильно, потому что именно так пользователь видит систему.
Мы как бы присоединяемся к нему — видим и тестируем ее так же.
Это все классно, но почему-то очень мало говорится про белый ящик.
Однажды мне и самому стало интересно, почему так. Что такое тестирование белого ящика?
Я полез разбираться. Начал искать источники. Качество русскоязычных оказалось очень низким, переведенных с английского на русский — чуть выше. И я добрался до англоязычных источников — до самого Гленфорда Майерса (G. Myers), который написал замечательную книгу «The Art of Software Testing».
Буквально во второй главе автор начинает говорить про тестирование белого ящика:
«To combat the challenges associated with testing economics, you should establish some strategies before beginning. Two of the most prevalent strategies include black-box testing and white-box testing…»
В конце в словаре Майерс дает некое определение тестированию белого ящика:
«White-box testing — A type of testing in which you examine the internal structure of a program».
Что же на практике? Майерс предлагает строить тестовые сценарии, ориентируясь на покрытие:
Все, о чем говорит Майерс, было 35 лет назад. Какой софт писали тогда и какой — сейчас? С какими кодовыми базами работали тогда — и сейчас? Очень многое изменилось. Покрытие — это, конечно, хорошо, и для его измерения есть много инструментов, о которых мы поговорим ниже. Но покрытие — это далеко не все. Особенно с учетом того, что мы живем в мире распределенных систем, где браслет с руки человека пробрасывает данные через телефон в облачные сервисы.
Что сейчас нужно понимать под тестированием белого ящика? Мы смотрим в код, понимаем структуру и зависимости, которые есть в этом коде, задаем вопросы, делаем выводы и проектируем тесты на основе этих данных. Мы выполняем эти тесты вручную или автоматически и на их основе получаем новые данные о состоянии нашей системы — о том, как она может или не может работать. Это и есть наш профит.
Зачем нам всем этим заниматься, если у нас есть черный ящик — то есть то, как пользователь видит систему? Ответ очень простой: жизнь сложна.
Это стек вызовов обычного современного энтерпрайзного приложения, написанного на языке Java:
Не только на Java все так многословно и обильно. На любом другом языке это будет выглядеть примерно так же. Что здесь есть?
Здесь есть вызовы веб-сервера; security framework-а, который делает авторизацию, аутентификацию, проверяет права и все остальное. Здесь есть web-фреймворк и еще один web-фреймворк (потому что в 2017 году нельзя просто так взять и написать энтерпрайзное приложение на одном web-фреймворке). Здесь есть фреймворки для работы с базой данных и преобразования объектов в столбцы, таблицы, колонки и все остальное. И здесь есть маленький желтый квадратик — это один вызов бизнес-логики. Все, что под и над ним, происходит в вашем приложении каждый раз.
Пытаясь подобраться к этой штуке где-то снаружи с черным ящиком (как это видит пользователь), вы очень много чего можете не протестировать. А иногда вам это очень надо, особенно когда поведение пользователей меняет что-то в security, пользователя перенаправляют в какие-то другие места или что-то происходит в базе данных. Черный ящик не позволяет вам этого сделать. Именно поэтому нужно залезать внутрь — в белый ящик.
Как это делать? Давайте посмотрим на практике.
Чтобы не было неправильных или завышенных ожиданий, давайте с самого начала проясним некоторые детали:
Чтобы мой дальнейший рассказ был более-менее структурированным, я разбил его на три уровня. Начнем с самого простого — easy level.
Как я говорил, мы смотрим в код и видим:
Исправление этого — самая первая и простая штука, которую можно сделать в области тестирования белого ящика. Со всем этим замечательно справляются инструменты статического анализа кода, которые сегодня уже достаточно комплексные — типа Sonar для Java и аналогов для ваших языков (на самом деле Sonar мультиязыковой и подходит практически всем).
Я не хочу долго здесь задерживаться. Про это есть куча интересных докладов.
Средний уровень сложности отличается масштабом. Когда вы работаете в маленькой компании или команде, вы единственный тестировщик, у вас есть три-четыре разработчика (как и в среднем по отрасли), 100 тыс. строк кода на всех, а код-ревью осуществляется метанием тапка ведущего разработчика в того, кто провинился, — какие-то специальные инструменты вам не нужны. Но так бывает редко.
Большие успешные проекты обычно «размазаны» на несколько офисов и команд разработки. А размер кодовой базы начинается с миллиона строк кода.
Когда в проекте очень много кода, разработчики начинают выстраивать формальные правила, по которым пишется этот код:
Иными словами, по мере роста объема кода возникают формальные правила, которые можно проверять. Соответственно, появляются инструменты, которые позволяют это делать.
Давайте посмотрим на примере.
ArchUnit позволяет в виде более-менее проблемно-ориентированного языка описывать формальные правила того, что должно или не должно быть в коде, и запихивать их в виде стандартов unit-тестов внутрь каждого проекта. Так изнутри проекта ArchUnit позволяет проверять, что в нем соблюдается «санитарный минимум».
Итак, у нас есть правило ArchRuleDefenition
:
@Test
public void testNoDirectUsagesOfSelenium() {
ArchRule rule = ArchRuleDefinition
.noClasses()
.that()
.resideInAPackage("org.example.out.test")
.should()
.accessClassesThat()
.resideInAPackage("..org.openqa.selenium..");
rule.check(classes);
}
Правило говорит, что ни в одном классе (.noClasses()
), который находится в соответствующим пакете с тестами (org.example.out.test
), не должно быть обращений напрямую к внутренностям Selenium (..org.openqa.selenium..
).
Давайте запустим этот тест. Он замечательно падает:
При этом он пишет, что у нас нарушено правило (когда класс, находящийся в таком-то пакете, стучится в классы, которые находятся в другом пакете). Что более ценно, в виде стек-трейса он показывает все строчки, где это правило не соблюдается.
ArchUnit — замечательный инструмент, который позволяет встраивать подобные штуки в CI/CD цикл, то есть писать внутри проекта тесты, которые проверяют какие-то архитектурные правила. Но у него есть один недостаток: он проверяет все тогда, когда код уже написан и куда-то закоммитчен (то есть сработает либо commit hook, который отклонит этот коммит, либо еще что-то). А бывают ситуации, когда нужно, чтобы плохой код вообще нельзя было написать.
Исходный код примера:
На прошлом Heisenbug-е летом 2017 мой коллега по цеху из Яндекса, Кирилл Меркушев, рассказывал о том, как кодогенерация решает проблемы автоматизации тестирования. Кто не смотрел его выступление — пожалуйста, посмотрите, видео есть тут [4].
На самом деле кодогенерация может решить много проблем. Она позволяет не только создавать код, который не хочется писать, но и запрещать создание кода, который не должен быть написан. Давайте посмотрим, как это работает.
Большая часть кодогенерации работает на процессинге аннотаций. У меня есть проект, где описана пара процессоров аннотаций, которые специфичны для мира Java-разработки, — в частности, аннотация Pojo. В программах на Java нет такого понятия как структуры. Отцы-основатели Java сейчас думают о том, чтобы ввести структуры в язык программирования. В Cи это уже было, у нас — еще нет (хотя прошло больше 40 лет). Но мы смогли выкрутиться — у нас есть Pojo (plain old java object), то есть объекты с полями, геттерами, сеттерами, но в них больше ничего нет — никакой логики.
У меня есть аннотация, которая характеризует собой объект Pojo, а также аннотация, которая характеризует собой Helper — это объект без состояния, в который напиханы всякие методы процедурного рода (чистая бизнес-логика). И у меня есть два процессора таких аннотаций.
Процессор аннотаций Pojo ищет в коде соответствующие аннотации, а когда находит, проверяет код на соответствие тому, что является (или не является) Pojo. Аналогично действует процессор аннотаций Helper (вот ссылка на аннотации и процессоры аннотаций [5]).
Как это все работает? У меня есть маленький проект, я запускаю в нем компиляцию:
Я вижу, что оно даже не компилируется:
Это происходит потому, что в этом проекте содержится код, который нарушает правила:
package a.b.c;
import annotations.Pojo;
@Pojo
public class AnotherFailed {
private long point;
}
В отличие от предыдущего примера, эта штука встраивается внутрь среды разработки, внутрь continuous integration, то есть позволяет охватывать больший контур внутри CI/CD-цикла.
Когда вы наигрались на предыдущих уровнях, вам хочется чего-то большего.
Для измерения покрытия кода, с тех пор, как Майерс написал свою книжку, появилось очень много разных инструментов. Они есть практически для каждого языка программирования. Здесь я привел только то, что я посчитал популярным по количеству ссылок на них в интернете (вы можете сказать, что это неправильно — я с вами соглашусь):
В некоторых языках программирования (для меня это было удивлением) — допустим, в Python и в Go — инструменты для измерения покрытия кода тестами встроены в сам язык.
Инструменты есть и, более того, существует интеграция этих инструментов со средами разработки, когда мы видим эту замечательную штучку слева, свидетельствующую о том, что этот кусок кода покрыт unit-тестами (зеленый цвет), а этот — нет (красный цвет).
И глядя на это в контексте unit-тестов, хочется задать вопрос — почему так нельзя сделать с интеграционными или с функциональными тестами? Где-то можно!
Но кроме тестов у нас есть пользователи. Тестировать можем все, что угодно (главное тестировать не фигню), но пользователи давят в какое-то одно место, потому что они этим пользуются 95% времени. И почему нельзя сделать такие же красивые полосочки, но только для кода, который используется или не используется?
На самом деле, так сделать можно. Давайте посмотрим, как.
Представьте, что я тестировщик этого приложения. И мне оно попадает на регрессионное тестирование («Срочно, горим, делаем мега-стартап, надо проверить, что работает, что не работает»). Я провожу с ним все эти манипуляции — все работает, мы отпускаем в релиз. Релиз проходит успешно, все хорошо.
Проходит полгода — ситуация повторяется. За полгода разработчики что-то там поменяли. Что именно, я не знаю. Могу ли я это узнать — это отдельный вопрос. Но самое главное — какой код теперь вызывается? Все ли я проверил нажатием одной единственной кнопки или не все? Понятно, что не все, но не пропустил ли я чего-то важного?
На эти вопросы можно найти ответы, если вместе с приложением запустить агент, снимающий с него покрытие.
Я использовал Jacoco. Можно брать любой, главное, чтобы вы потом могли понять, что он вам намерял. В результате работы агента у нас появился файлик jacoco.exec:
Из этого файлика, исходного приложения и бинарника приложения можно создать отчет, из которого будет видно, как это все работает.
У меня есть маленький скрипт, который проанализировал эту штуку и создал папку html:
Скрипт показывает вот такой отчет:
В процессе тестирования я что-то продавил руками, а что-то нет — в разном процентном соотношении. Но так как мы не стесняемся заглядывать в белый ящик и смотреть, что же происходит внутри приложения, мы знаем, куда нам нужно давить.
В этом отчете зеленым подсвечиваются те строчки, которые я «продавил». Красным — которые не продавил.
Если мы прочтем этот код более-менее вдумчиво (даже не вникая в то, что происходит внутри), мы сможем понять, что никакую работу, связанную с отказом сети, я не продавил. Также я не проверил кейсы получения нехорошего статус-кода (что мы не авторизованы запрашивать репозитории этой организации).
Для проверки падения сети можно обрушить сетку или внедрить Fault Injection testing, а можно написать другой Fault Injection implementation, положив ее в каталог с приложением, получать статус-код не 200, а, например, 401.
Пытаясь ответить на вопросы о том, что проверяется нашими тестами, куда давят наши пользователи и как на самом деле одно соотносится с другим, мы в Одноклассниках создали сервис, который умеет сводить все воедино. Мы же делаем пользовательский сервис. Мы можем тестировать какой-то забытый уголок нашего большого портала, куда никто не заходит, но какая в этом ценность?
Сначала мы назвали его Cover. Но потом из-за опечатки одного из наших инженеров мы переименовали его в KOVЁR.
KOVЁR знает о нашем цикле разработки ПО, в частности, когда нужно включить замер покрытия, когда нужно его выключить, когда с этого надо срендерить отчеты. И KOVЁR позволяет нам сравнивать отчеты по тому, что было, допустим, на прошлой неделе, и на этой; по тому, что мы сделали автотестами, и тому, что продавили люди руками.
Выглядит это так (это реальные скриншоты с KOVЁR):
Получаем side-by-side сравнение одного и того же кода. Слева находятся автотесты, справа — пользователи. Красным подсвечено то, что не продавлено, зеленым то, что продавлено (в данном случае автотесты продавливают конкретный кусок бизнес-логики намного лучше, чем пользователи).
Как вы понимаете, все может корректироваться: лево и право могут меняться, используемые цвета — тоже.
В итоге получаем такую довольно простую матрицу 2х2, характеризующую код:
Там, где у нас есть покрытие и автотестами, и людьми — его нужно сравнивать, и с этим KOVЁR работает. Где есть покрытие автотестами, но нет людей, надо хорошо подумать. С одной стороны, это может быть мертвый код — очень большая проблема современной разработки. С другой — это может быть функционал, который используется людьми в каких-то экстренных обстоятельствах (восстановление пользователей, разблокировка, бэкап, восстановление из бэкапа — то, что вызывается крайне редко).
Там, где нет автотестов, но есть люди, очевидно, надо писать код, покрывая эти места, и стремиться к разумному, доброму, вечному. А где нет ни автотестов, ни людей — в первую очередь нужно вставить какие-то метрики и проверить, что этот код действительно никогда не вызывается. После этого надо безжалостно его удалить.
Инструменты Code Coverage уже существуют, и надо их просто интегрировать к себе. С ними вы сможете:
Существует классическая математическая задачка о компоновке рюкзака: как упаковать все вещи в рюкзак, чтобы они туда поместились и осталось как можно больше пространства. Я думаю, многие из вас слышали про нее. Давайте посмотрим на нее в контексте тестирования.
Предположим, у меня есть 10 автотестов. Они выглядят так:
В реальности каждый автотест бегает разное время. Поэтому в определенный момент времени они выглядят вот так:
И у нас есть два ресурса, на которых мы их запускаем:
Я не знаю, что это — jenkins slave, виртуальные машины, docker-контейнеры, телефоны — все, что угодно.
Если мы возьмем эти 10 тестов и раскинем их на два ресурса поровну, получим такую картину:
Эта картинка — не хорошая и не плохая, но в ней есть одна особенность: первый ресурс у нас достаточно долгое время простаивает, а тестирование на втором ресурсе еще идет.
Не меняя количество тестов на каждом из этих ресурсов, можно просто перегруппировать их и получить вот такую картинку:
В каждом ресурсе осталось по пять тестов, но простой сократился — мы сэкономили примерно 20% времени тестирования. Когда мы первый раз врубили у себя эту оптимизацию, она реально сэкономила нам 20%. То есть эта цифра не с потолка, а из практики.
Если рассматривать эту закономерность дальше, то скорость тестов — это всегда функция от того, сколько у вас есть ресурсов и сколько у вас есть тестов. Дальше вы должны ее балансировать и как-то оптимизировать.
Почему это важно?
Потому что не все всегда одинаково. Предположим, к вам кто-то прибегает на ваш Continuous integration server и говорит, что нам нужно срочно запустить тесты — проверить фикс и сделать это как можно быстрее.
Вы можете пойти на поводу у этого человека и дать ему все возможные ресурсы для запуска тестов.
Правда может оказаться в том, что их фикс не очень важен по сравнению с текущим релизом, который должен выкатываться через два часа. Это первое.
А второе — тестов на самом деле не так много, как у вас ресурсов. То есть картинка, которую я показал раньше, где у вас 10 тестов и два ресурса, — это очень большое упрощение. Ресурсов может быть 200, а тестов — 10 тыс. И эта игра с тем, сколько кому дать ресурсов, начинает влиять на всех.
Чтобы правильно играть в эту игру, нужно всегда иметь ответы на два вопроса: сколько у вас ресурсов для запуска и сколько тестов.
Если вы будете достаточно долго думать над вопросом о том, сколько у вас ресурсов и сколько у вас тестов (особенно над последним), рано или поздно вы придете к мысли о том, что было бы неплохо парсить код ваших тестов и разбираться в том, что же в нем происходит:
Вам эта мысль может показаться безумной, но не гоните ее сразу. Все среды разработки уже делают это, чтобы показывать вам вот такие подсказки:
Причем они занимаются парсингом не только кода, но и всех зависимостей в нем.
Они умеют это делать. Более того, все среды разработки делают это хорошо, а некоторые даже поставляют библиотеки, которые позволяют решать такие задачи буквально в шесть строчек (по крайней мере, для Java [7]).
В этих шести строках вы разбираете и полностью парсите какой-то кусок кода. Вы можете достать из него любую метаинформацию: сколько в нем полей, методов, конструкторов — чего угодно, в том числе тестов.
И имея все это в голове, мы создали сервис, который называется Berrimor.
BERRIMOR умеет говорить «овсянка, сэр!», а еще он умеет:
BERRIMOR поставляет все эти данные наружу.
Я мог бы показать вам интерфейс BERRIMOR, но вы бы все равно ничего там равно бы. Вся его мощь кроется внутри API.
В 2010 году я читал лекции Сергея Архипенко по управлению программными проектами и мне запомнилась вот эта вот цитата:
"…реальность, которая заключена в особой специфике производства программ, по сравнению с любой другой производственной деятельностью, потому что то, что производят программисты – нематериально, это коллективные ментальные модели, записанные на языке программирования" (Сергей Архипенков, Лекции по управлению программными проектами, 2009).
Ключевое слово — коллективные. У людей есть почерк, но не у всех он хороший. У программистов тоже есть почерк (и также не всегда хороший). Между людьми существуют какие-то взаимосвязи: кто-то пишет фичу, кто-то ее патчит, кто-то ее чинит. Эти зависимости есть внутри каждого коллектива, внутри каждой команды разработки. И они влияют на качество того, что происходит в проекте.
Социальный анализ кода — нарождающаяся дисциплина. Я выделил три видео, которые есть в открытом доступе и могут помочь вам понять, что же это такое.
Социальный анализ кода позволяет:
Чуть подробнее про технический долг. У меня есть слабая гипотеза о техническом долге.
У нас в «Одноклассниках» это выглядит так:
Когда я что-то пишу и коммитчу, я указываю ссылку на тикет в Jira. В силу NDA я не могу показывать вам социальный анализ кода на примере репозиториев «Одноклассников». Я покажу на примере open source-проекта Kafka.
У Kafka есть открытый issue -трекер, открытый репозиторий с кодом:
Давайте посмотрим, что же там происходит.
Итак, у меня есть (маленькое утилитное приложение [15]), которое поднимает все коммиты в этом репозитории и разбирает все комментарии к ним, обеспечивая поиск по регулярному выражению Pattern.compile("KAFKA-\d+")
коммитов, которые ссылаются на какой-то тикет.
В консоли видно, что коммитов всего 4246, а коммитов без такого упоминания — 1562. То есть точность анализа на треть меньше, чем хотелось бы.
Дальше мы поднимаем каждый коммит, составляем из него индекс — какие файлы в нем менялись (под какой тикет). Составляем все эти индексы в большой хэшмап: имя файла — список тикетов, по которым этот файл менялся. Вот как это выглядит:
Например, у нас есть файл KafkaApis и рядом огромный список issue, по которым он менялся (API меняется часто).
Дальше мы идем в issue-трекер Kafka и определяем, по каким issue эта штука менялась - это был баг, фича, оптимизация? На выходе мы получаем маленький хэш, где написано, что это за штука, и какой у нее приоритет (это все только баги):
итоге мы получаем вот такой вывод:
Где мы пишем, какой процент изменений был в том или ином файле:
Например, для верхней строки общее количество тикетов, которые прошло в коммитах через этот файл, — 231, из них багов — 128 и, соответственно, 128 делим на 231 — получаем 55% — доля изменений. С большой вероятностью технический долг сосредоточен именно в этих файлах.
Я вам показал шесть разных примеров. Это далеко не все, что существует. Но это значит, что белый ящик — это в первую очередь стратегия. Как вы ее будете реализовывать на вашем проекте — вам виднее. Надо думать, не надо бояться залезть в код. Там всегда лежит вся правда о вашем проекте. Поэтому читайте код, пишите код, вмешивайтесь в тот код, который пишут программисты.
Автор: Евгений Трифонов
Источник [21]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/testirovanie/278899
Ссылки в тексте:
[1] Исходный код примера: https://goo.gl/kGKF9T
[2] Library : https://goo.gl/GcuLC3
[3] Test Project: https://goo.gl/eynhCr
[4] тут: https://www.youtube.com/watch?v=9mF0zFW7cDQ
[5] ссылка на аннотации и процессоры аннотаций: https://github.com/kronar/qa-annotations
[6] Исходный код примера: https://goo.gl/YJfQQB
[7] для Java: http://bit.ly/2j8gype
[8] Mining Repository Data to Debug Software Development Teams: https://www.youtube.com/watch?v=HJg5l9KTLBk
[9] Seven Secrets of Maintainable Codebases: https://www.youtube.com/watch?v=0oDporwhToQ
[10] How Flaky Tests in Continuous Integration: Current Practice at Google and Future Directions: https://youtu.be/CrzpkF1-VsA?list=PLSIUOFhnxEiAeGHYoBZCvEMY5wCOIpyOM&t=1895
[11] https://issues.apache.org/jira/projects/KAFKA: https://issues.apache.org/jira/projects/KAFKA
[12] https://github.com/apache/kafka: https://github.com/apache/kafka
[13] http://bit.ly/2yfc7lA: http://bit.ly/2yfc7lA
[14] http://bit.ly/2wO5ByG: http://bit.ly/2wO5ByG
[15] маленькое утилитное приложение: https://github.com/kronar/social-coding-analysis/blob/master/src/main/java/App.java
[16] Heisenbug 2018 Piter: https://heisenbug-piter.ru/
[17] Вспомогательные приемы при тестировании микросервисов: https://heisenbug-piter.ru/talks/2018/spb/1jny85xvcg2qqg0c66ccwq/
[18] Web Security Testing Starter Kit: https://heisenbug-piter.ru/talks/2018/spb/chexr4uzvkiqciysaogyo/
[19] Бета-тестирование ВКонтакте: https://heisenbug-piter.ru/talks/2018/spb/beyolylrla4eq244mg4oi/
[20] По следам Wild West testing: необычные трюки для обычных проблем: https://heisenbug-piter.ru/talks/2018/spb/3zx0l1uelgac2asiscokki/
[21] Источник: https://habr.com/post/354434/?utm_source=habrahabr&utm_medium=rss&utm_campaign=354434
Нажмите здесь для печати.