Покрытие программного кода
19.2.2.3. По компонентам логических условий
Для более полного анализа компонент условий в логических операторах существует несколько методов, учитывающих структуру компонент условий и значения, которые они принимают при выполнении тестовых примеров.
19.2.2.4. Покрытие по условиям (Condition Coverage)
Для обеспечения полного покрытия по данному методу каждая компонента логического условия в результате выполнения тестовых примеров должна принимать все возможные значения, но при этом не требуется, чтобы само логическое условие принимало все возможные значения. Так, например, при тестировании следующего фрагмента
для покрытия по условиям потребуется два тестовых примера:
При этом значение логического условия будет принимать значение только true; таким образом, при полном покрытии по условиям не будет достигаться покрытие по веткам.
19.2.2.5. Покрытие по веткам/условиям (Condition/Decision Coverage)
Данный метод сочетает требования предыдущих двух методов – для обеспечения полного покрытия необходимо, чтобы как логическое условие, так и каждая его компонента приняла все возможные значения.
Для покрытия рассмотренного выше фрагмента с условием
потребуется 2 тестовых примера:
19.2.2.6. Покрытие по всем условиям (Multiple Condition Coverage)
Для выявления неверно заданных логических функций был предложен метод покрытия по всем условиям. При данном методе покрытия должны быть проверены все возможные наборы значений компонент логических условий. Т.е. в случае n компонент потребуется 2n тестовых примеров, каждый из которых проверяет один набор значений, Тесты, необходимые для полного покрытия по данному методу, дают полную таблицу истинности для логического выражения.
Несмотря на очевидную полноту системы тестов, обеспечивающей этот уровень покрытия, данный метод редко применяется на практике в связи с его сложностью и избыточностью.
Еще одним недостатком метода является зависимость количества тестовых примеров от структуры логического выражения. Так, для условий, содержащих одинаковое количество компонент и логических операций:
потребуется разное количество тестовых примеров. Для первого случая для полного покрытия нужно 6 тестов, для второго – 11.
19.2.2.7. Метод MC/DC для уменьшения количества тестовых примеров при 3-м уровне покрытия кода
Для уменьшения количества тестовых примеров при тестировании логических условий фирмой Boeing был разработан модифицированный метод покрытия по веткам/условиям (Modified Condition/ Decision Coverage или MC/DC). Данный метод широко используется при верификации бортового авиационного программного обеспечения согласно процессам стандарта DO-178B.
Для обеспечения полного покрытия по этому методу необходимо выполнение следующих условий:
Покрытие по этой метрике требует достаточно большого количества тестов для того, чтобы проверить каждое условие, которое может повлиять на результат выражения, однако это количество значительно меньше, чем требуемое для метода покрытия по всем условиям.
С учетом сделанных обозначений фрагмент кода может быть записан так:
Для тестирования первого условия по MC/DC надо показать независимость результата (т.е. функции A || B ) от каждого аргумента. Соответственно, для этого используются три тестовых примера:
Итоговая таблица тестовых примеров для покрытия по MC/DC будет выглядеть следующим образом:
| 1 | 2 | 3 | 4 | |
|---|---|---|---|---|
| prev | операнд(A=0, B=0) | оператор(A=1,B=0) | унарный оператор (A=0, B=1) | оператор |
| ShowMessage | false | false | false | true |
Количество тестовых примеров можно сократить до 3, если совместить примеры 3 и 4. Такое совмещение не повлияет на покрытие.
| 1 | 2 | 3 | |
|---|---|---|---|
| prev | операнд(A=0, B=0) | оператор(A=1,B=0) | унарный оператор (A=0, B=1) |
| ShowMessage | false | false | true |
Пример 2. Для покрытия по MC/DC более сложных выражений рассмотрим следующий участок кода:
В третьем тестовом примере значения A и B могут быть получены сразу, т.е. имеем
Таким образом, мы построили 4 тестовых примера для проверки данного участка кода.
| 1 | 2 | 3 | 3 | |
|---|---|---|---|---|
| A | 1 (true) | 1 (true) | 1 (true) | 0 (false) |
| B | 0 (false) | 0 (false) | 1 (true) | 1 (true) |
| C | 0 (false) | 1 (true) | 0 (false) | 0 (false) |
Модульные тесты в ABAP. Часть третья. Всяческая суета
Эта статья ориентирована на ABAP-разработчиков в системах SAP ERP. Она содержит много специфических для платформы моментов, которые малоинтересны или даже спорны для разработчиков, использующих другие платформы.
Будем меряться
Считается, что главной метрикой качества тестов является покрытие. В разработческих интернетах часто можно встретить формулировки в стиле “полное покрытие”. Как правило, под полным покрытием понимается некий абсолют в 100.00%.
Процент покрытия – цифра сомнительная, ровно настолько же сомнительная, как и “средняя температура по больнице”. Процент покрытия по проекту – это среднее покрытие его частей. То есть: Модуль-1 имеет покрытие 80%, Модуль-2 имеет покрытие 20%, в среднем покрытие будет равно 50%, если допустить что модули примерно равны по содержимому. А верно ли что 80% в четыре раза лучше чем 20%?
Среднее бывает разное. В ABAP UNIT есть три различные метрики покрытия:
NB. Пул подпрограмм проще для демонстрации, чем группа функций или класс с методами. Пул подпрограмм описывается существенно меньшим количеством букв, чем класс. Параметры вынесены за рамки определений. В рамках этой маленькой демонстрации существенной разницы нет. И вообще: все переменные вымышлены, любые совпадения с продуктивным кодом случайны.
И предположим, что мы написали по одному простому тесту на каждую подпрограмму, функцию, метод. Для всех подпрограмм мы будем использовать значения [A = 7, B = 77].
NB: Пусть пока будет общая инициализация, а проверку результата опустим.
Procedure coverage
Это самый простой случай, можно посчитать на пальцах. Покрытие по процедурам будет 100% = ( 1 + 1 + 1 ) / ( 1 + 1 + 1 ) * 100.
Statement coverage
А если для тех же процедур мы посчитаем количество инструкций?
Каждая процедура содержит разное количество инструкций. Причём при заданных входных параметрах будут вызваны не все инструкции:
Если мы посчитаем метрику по инструкциям, то будет 71% = ( 3 + 5 + 2 ) / ( 3 + 8 + 3 ) * 100.
Рассмотрим работу метрики на DO_SOMETHING_ELSE. Инструменты разработки ABAP могут раскрасить строки исходного кода в соответствии с метрикой:
Наглядно, быстро, понятно. Просто удивительно, даже не ожидал такого от ABAP.
Из этой раскраски становится очевидно, что если бы мы взяли другие исходные параметры, то процент покрытия могу бы быть другим. В случае [A = 77, B = 7]:
При этом становится очевидным, что полного покрытия по данной метрике можно достичь только используя более одного тестового сценария. Например, при двух тестах [A = 77, B = 7] и [A = 7, B = 7777] всё позеленеет:
Таким образом метрика выходит на 100%. Можно ненадолго успокоиться.
Branch coverage
Эта метрика работает несколько сложнее. Она берёт все инструкции, которые могут вызвать ветвление, и проверяет их на то, что каждая такая инструкция выполняется в обе стороны.
Посмотрим на базе последнего примера:
Первая инструкция [IF A > B] на двух тестах отработала два раза: один раз по TRUE [A = 77, B = 7] и один раз по FALSE [A = 7, B = 7777].
А вот вторая инструкция [IF D > 1000] отработала только один раз на TRUE [A = 7, B = 7777].
Сам вызов функции считается за безусловную единицу, плюс первый IF даёт два из двух, второй IF даёт только единицу из двух. Значит наша метрика будет равна 80% = (1 + 2 + 1 ) / (1 + 2 + 2) * 100.
И тут уже выходит что для одной функции двух тестов уже мало, а нужно три. К предыдущим двум можно ещё добавить сценарий [A = 7, B = 77], чтобы второй IF отработал на FALSE.
После добавления третьего сценария метрика по этой функции вышла на 100%.
А что же с DO_NOTHING, спросите вы? Не существует такого теста, чтобы метрика по ветвям или инструкциям вышла на 100%. Очевидно, что функция требует рефакторинга, без которого выйти на полное покрытие не получится. Эту функцию следует или удалить, или она должна превратиться из DO_NOTHING в DO_SOMETHING_COMPLETELY_DIFFERENT.
Сто процентов!
Жаль нельзя написать ещё больше тестов и получить более 100%.
Понятно, что метрика Procedure coverage менее показательна в деталях. К ней можно внимательно присматриваться только на ранних этапах, если кода много а тестов ещё почти нет. А вот к какой метрике из двух оставшихся приглядываться после? Если первая метрика просто показывает насколько широко вы охватили функционал, то последние показывают, насколько вы его качественно охватили.
Как вы заметили, можно получить 100% по инструкциям, но при этом не будет 100% по ветвям. Но не наоборот (или я не могу придумать такой пример). Если вы уж получили 100% по ветвям, то значит вы зашли во все закоулки и все инструкции отработали. Но кому-то может показаться, что метрика по ветвям даёт менее показательные весовые коэффициенты в среднем, так как игнорирует один из явных весовых показателей – количество строк кода, то есть количество инструкций.
BTW: Да, пустая процедура даёт 100% показатели!
Уговор есть уговор
Для работы ABAP Unit неважно:
Следовательно, у нас по каждому пункту должна быть некоторая общая условная договорённость, облегчающая общее восприятие картины. Вроде соглашения по именованию или форматированию.
Тестовых классов должно быть ровно столько сколько нужно. Как минимум, каждый большой объект (группа функций, программа, класс) должен иметь один тестовый класс, можно больше.
Если у вас простая группа из нескольких связанных функций, то к ней достаточно и одного класса. А вот если в вашей группе есть шесть пачек малосвязанных функций, то здесь здесь должен скорее возникнуть вопрос “А сколько должно быть групп функций?”, а это тема для совсем другого разговора.
После корректного ответа на данный вопрос можно взять метод SETUP в качестве критерия делимости. Такой метод в классе должен быть один, вызывается он автоматически перед каждым тестовым методом.
Каждый сценарий должен дать отдельный тестовый метод, наименование метода должно прямо выводиться из тестируемого кода.
Один из принципов модульного тестирования: тестовый класс должен тестировать только тот код, в юрисдикции которого он находится. И хотя тесты могут находиться в любом месте исходного кода, но стоит отделять работающую функциональность от тестов.
Вот мастер для групп функций создаёт отдельную include-программу по предопределённому шаблону: например: LZFI_BTET99 для группы функций ZFI_BTE. Ничего плохого в этом не вижу, надо принимать за образец и продолжать в том же духе.
Также и в программах типа REPORT: пишите тесты строго в одной отдельной include-программе, с именем по шаблону.
Впрочем, никому не могу запретить писать всё вперемешку: код, его тест, код, его тест…
Когда?
Нельзя каждые пять минут запускать полный цикл модульных тестов. Но, как минимум, перед деблокированием запроса необходимо запускать тест причастных объектов.
Подытожу
Просто пачка тезисов для подведения черты:
Прямая реальная польза от тестов будет только в те моменты, когда по прошествии времени тесты будут провалены, когда кто-то будет допиливать эту функциональность.
Потому что умение правильно падать – самый лучший способ избежать травм. Если это верно для каратистов и велосипедистов, значит и для программистов тоже будет нелишним. Лучше правильно упасть плохо крутя педали, чем неправильно упасть хорошо крутя педали. Умение правильно падать важнее правильной экипировки.
А прямо сейчас можно извлечь только косвенную пользу:
Тестирование программного кода (покрытия)
6.3. Покрытие программного кода
6.3.1. Понятие покрытия
Один из часто используемых методов определения полноты системы тестов является определение отношения количества тест-требований, для которых существуют тестовые примеры, к общему количеству тест-требований. Т.е. в данном случае речь идет о покрытии тестовыми примерами тест-требований. В качестве единицы измерения степени покрытия здесь выступает процент тест-требований, для которых существуют тестовые примеры, называемый процентом покрытых тест-требований.
Покрытие требований позволяет оценить степень полноты системы тестов по отношению к функциональности системы, но не позволяет оценить полноту по отношению к ее программной реализации. Одна и та же функция может быть реализована при помощи совершенно различных алгоритмов, требующих разного подхода к организации тестирования.
Для более детальной оценки полноты системы тестов при тестировании стеклянного ящика анализируется покрытие программного кода, называемое также структурным покрытием.
6.3.2. Уровни покрытия
6.3.3. По строкам программного кода (Statement Coverage)
Для обеспечения полного покрытия программного кода на данном уровне необходимо, чтобы в результате выполнения тестов каждый оператор был выполнен хотя бы один раз.
Особенность данного уровня покрытия состоит в том, что на нем затруднен анализ покрытия некоторых управляющих структур.
Например, для полного покрытия всех строк следующего участка программного кода на языке C достаточно одного тестового примера:
Другой особенностью данного метода является зависимость уровня покрытия от структуры программного кода. На практике часто не требуется 100% покрытия программного кода, вместо этого устанавливается допустимый уровень покрытия, например 75%. Проблемы могут возникнуть при покрытии следующего фрагмента программного кода:
6.3.3.1. По веткам условных операторов (Decision Coverage)
В отличие от предыдущего уровня покрытия данный метод учитывает покрытие условных операторов с пустыми ветками. Так, для покрытия по веткам участка программного кода
необходимы два тестовых примера:
Особенность данного уровня покрытия заключается в том, что на нем не учитываются логические выражения, значения компонент которых получаются вызовом функций. Например, на следующем фрагменте программного кода
полное покрытие по веткам может быть достигнуто при помощи двух тестовых примеров:
6.3.3.2. По компонентам логических условий
Для более полного анализа компонент условий в логических операторах существует несколько методов, учитывающих структуру компонент условий и значения, которые они принимают при выполнении тестовых примеров.
6.3.3.3. Покрытие по условиям (Condition Coverage)
Для обеспечения полного покрытия по данному методу каждая компонента логического условия в результате выполнения тестовых примеров должна принимать все возможные значения, но при этом не требуется, чтобы само логическое условие принимало все возможные значения. Так, например, при тестировании следующего фрагмента:
для покрытия по условиям потребуется два тестовых примера:
При этом значение логического условия будет принимать значение только true, таким образом, при полном покрытии по условиям не будет достигаться покрытие по веткам.
6.3.3.4. Покрытие по веткам/условиям (Condition/Decision Coverage)
Для покрытия рассмотренного выше фрагмента с условием condition1 | condition2 потребуется 2 тестовых примера:
6.3.3.5. Покрытие по всем условиям (Multiple Condition Coverage)
Для выявления неверно заданных логических функций был предложен метод покрытия по всем условиям. При данном методе покрытия должны быть проверены все возможные наборы значений компонент логических условий. Т.е. в случае n компонент потребуется 2 n тестовых примеров, каждый из которых проверяет один набор значений, Тесты, необходимые для полного покрытия по данному методу, дают полную таблицу истинности для логического выражения.
Несмотря на очевидную полноту системы тестов, обеспечивающей этот уровень покрытия, данный метод редко применяется на практике в связи с его сложностью и избыточностью.
Еще одним недостатком метода является зависимость количества тестовых примеров от структуры логического выражения. Так, для условий, содержащих одинаковое количество компонент и логических операций:
Conditional coverage
Environment-based logic
My first attempt was to create a route_visit function to do the correct thing for all releases. To make it work I also had to define PY38 constant like so:
Here’s the problem with my solution: I was either covering if PY38: branch on python3.8 build or else: branch on other releases. I was never covering 100% of my program. Because it is literally impossible.
Common pitfalls
Open-source libraries usually face this problem. They are required to work with different python versions, 3rd party API changes, backward compatibility, etc. Here are some examples that you probably have already seen somewhere:
Or this was a popular hack during 2 / 3 days:
With all these examples in mind, one can be sure that 100% of coverage is not possible.
The common scenario to still achieve a feeling of 100% coverage for these cases was:
Here’s how the first way looks like:
Let’s be honest: these solutions are dirty hacks. But, they do work. And I personally used both of them countless times in my life.
Here’s the interesting thing: aren’t we supposed to test these complex integrational parts with the most precision and observability? Because that’s where our application breaks the most: integration parts. And currently, we are just ignoring them from coverage and pretending that this problem does not exist.
And for this reason, this time I felt like I am not going to simply exclude my compatibility logic. I got an idea for a new project.
Conditional coverage
First, we would need to install it:
And then we would have to configure coverage and the plugin itself:
Notice this rules key. It is the most important thing here. The rule (in this context) is some predicate that tells: should we include lines behind this specific pragma in our coverage or not. Here are some examples:
It is pretty clear what we are doing here: we are defining pairs of predicates to include this code if some condition is true and another code in the opposite case.
Here’s how our previous examples would look like with these magic comments:
What does it say? If we are running on py-lt-38 ignore if PY38: part. But, cover else: case. Because it is going to be executed and we know it. And we need to know how good did we cover it. On the other hand, if we are running on py-gte-38 then cover if PY38: case and leave else: alone.
And we can test that everything works correctly. Let’s add some nonsense into our PY38 branch to see if it is going to be covered by python3.8 build:
As we can see: green signs show which lines were fully covered, the yellow line indicates that branch coverage was not full, and the red line indicates that the line was not covered at all. And here’s an example of grey or ignored lines under the opposed condition:
Here you can find the full real-life source code for this sample.
And here’s one more example with django to show you how external packages can be handled:
Conclusion
I hope you got the idea. Conditional coverage allows you to add or ignore lines based on predicates and collecting required bits of coverage from every run, not just ignoring complex conditions and keeping our eyes wide shut. Remember, that the code we need to cover the most!
By the way, we have all kinds of helpers to query your environment:
This little plugin is going to be really helpful for library authors that have to deal with compatibility and unfixed environments. And coverage-conditional-plugin will surely cover their backs! Give it a star on Github if you like this idea. And read the project docs to learn more.
Белый ящик Пандоры
Обсуждая тестирование, чаще всего спикеры говорят об особенностях подхода, известного как «черный ящик». Но здесь мы поговорим о противоположном сценарии — «белом ящике», позволяющем формулировать вопросы к коду, понимая его внутреннюю структуру.
В основе статьи — расшифровка доклада Никиты Макарова (Одноклассники) с нашей декабрьской конференции 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».
Что же на практике? Майерс предлагает строить тестовые сценарии, ориентируясь на покрытие:
Что сейчас нужно понимать под тестированием белого ящика? Мы смотрим в код, понимаем структуру и зависимости, которые есть в этом коде, задаем вопросы, делаем выводы и проектируем тесты на основе этих данных. Мы выполняем эти тесты вручную или автоматически и на их основе получаем новые данные о состоянии нашей системы — о том, как она может или не может работать. Это и есть наш профит.
Зачем нужен белый ящик?
Зачем нам всем этим заниматься, если у нас есть черный ящик — то есть то, как пользователь видит систему? Ответ очень простой: жизнь сложна.
Это стек вызовов обычного современного энтерпрайзного приложения, написанного на языке Java:
Не только на Java все так многословно и обильно. На любом другом языке это будет выглядеть примерно так же. Что здесь есть?
Здесь есть вызовы веб-сервера; security framework-а, который делает авторизацию, аутентификацию, проверяет права и все остальное. Здесь есть web-фреймворк и еще один web-фреймворк (потому что в 2017 году нельзя просто так взять и написать энтерпрайзное приложение на одном web-фреймворке). Здесь есть фреймворки для работы с базой данных и преобразования объектов в столбцы, таблицы, колонки и все остальное. И здесь есть маленький желтый квадратик — это один вызов бизнес-логики. Все, что под и над ним, происходит в вашем приложении каждый раз.
Пытаясь подобраться к этой штуке где-то снаружи с черным ящиком (как это видит пользователь), вы очень много чего можете не протестировать. А иногда вам это очень надо, особенно когда поведение пользователей меняет что-то в security, пользователя перенаправляют в какие-то другие места или что-то происходит в базе данных. Черный ящик не позволяет вам этого сделать. Именно поэтому нужно залезать внутрь — в белый ящик.
Как это делать? Давайте посмотрим на практике.
Практика
Чтобы не было неправильных или завышенных ожиданий, давайте с самого начала проясним некоторые детали:
Easy level
Как я говорил, мы смотрим в код и видим:
Я не хочу долго здесь задерживаться. Про это есть куча интересных докладов.
Medium level
Средний уровень сложности отличается масштабом. Когда вы работаете в маленькой компании или команде, вы единственный тестировщик, у вас есть три-четыре разработчика (как и в среднем по отрасли), 100 тыс. строк кода на всех, а код-ревью осуществляется метанием тапка ведущего разработчика в того, кто провинился, — какие-то специальные инструменты вам не нужны. Но так бывает редко.
Большие успешные проекты обычно «размазаны» на несколько офисов и команд разработки. А размер кодовой базы начинается с миллиона строк кода.
Когда в проекте очень много кода, разработчики начинают выстраивать формальные правила, по которым пишется этот код:
Давайте посмотрим на примере.
ArchUnit
ArchUnit позволяет в виде более-менее проблемно-ориентированного языка описывать формальные правила того, что должно или не должно быть в коде, и запихивать их в виде стандартов unit-тестов внутрь каждого проекта. Так изнутри проекта ArchUnit позволяет проверять, что в нем соблюдается «санитарный минимум».
Итак, у нас есть правило ArchRuleDefenition :
Давайте запустим этот тест. Он замечательно падает:
При этом он пишет, что у нас нарушено правило (когда класс, находящийся в таком-то пакете, стучится в классы, которые находятся в другом пакете). Что более ценно, в виде стек-трейса он показывает все строчки, где это правило не соблюдается.
ArchUnit — замечательный инструмент, который позволяет встраивать подобные штуки в CI/CD цикл, то есть писать внутри проекта тесты, которые проверяют какие-то архитектурные правила. Но у него есть один недостаток: он проверяет все тогда, когда код уже написан и куда-то закоммитчен (то есть сработает либо commit hook, который отклонит этот коммит, либо еще что-то). А бывают ситуации, когда нужно, чтобы плохой код вообще нельзя было написать.
Annotation Processing
Исходный код примера:
На самом деле кодогенерация может решить много проблем. Она позволяет не только создавать код, который не хочется писать, но и запрещать создание кода, который не должен быть написан. Давайте посмотрим, как это работает.
Большая часть кодогенерации работает на процессинге аннотаций. У меня есть проект, где описана пара процессоров аннотаций, которые специфичны для мира Java-разработки, — в частности, аннотация Pojo. В программах на Java нет такого понятия как структуры. Отцы-основатели Java сейчас думают о том, чтобы ввести структуры в язык программирования. В Cи это уже было, у нас — еще нет (хотя прошло больше 40 лет). Но мы смогли выкрутиться — у нас есть Pojo (plain old java object), то есть объекты с полями, геттерами, сеттерами, но в них больше ничего нет — никакой логики.
У меня есть аннотация, которая характеризует собой объект Pojo, а также аннотация, которая характеризует собой Helper — это объект без состояния, в который напиханы всякие методы процедурного рода (чистая бизнес-логика). И у меня есть два процессора таких аннотаций.
Процессор аннотаций Pojo ищет в коде соответствующие аннотации, а когда находит, проверяет код на соответствие тому, что является (или не является) Pojo. Аналогично действует процессор аннотаций Helper (вот ссылка на аннотации и процессоры аннотаций).
Как это все работает? У меня есть маленький проект, я запускаю в нем компиляцию:
Я вижу, что оно даже не компилируется:
Это происходит потому, что в этом проекте содержится код, который нарушает правила:
В отличие от предыдущего примера, эта штука встраивается внутрь среды разработки, внутрь continuous integration, то есть позволяет охватывать больший контур внутри CI/CD-цикла.
Nightmare level
Когда вы наигрались на предыдущих уровнях, вам хочется чего-то большего.
Покрытие кода
Для измерения покрытия кода, с тех пор, как Майерс написал свою книжку, появилось очень много разных инструментов. Они есть практически для каждого языка программирования. Здесь я привел только то, что я посчитал популярным по количеству ссылок на них в интернете (вы можете сказать, что это неправильно — я с вами соглашусь):
Инструменты есть и, более того, существует интеграция этих инструментов со средами разработки, когда мы видим эту замечательную штучку слева, свидетельствующую о том, что этот кусок кода покрыт 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 автотестов. Они выглядят так:
В реальности каждый автотест бегает разное время. Поэтому в определенный момент времени они выглядят вот так:
И у нас есть два ресурса, на которых мы их запускаем:
Если мы возьмем эти 10 тестов и раскинем их на два ресурса поровну, получим такую картину:
Эта картинка — не хорошая и не плохая, но в ней есть одна особенность: первый ресурс у нас достаточно долгое время простаивает, а тестирование на втором ресурсе еще идет.
Не меняя количество тестов на каждом из этих ресурсов, можно просто перегруппировать их и получить вот такую картинку:
В каждом ресурсе осталось по пять тестов, но простой сократился — мы сэкономили примерно 20% времени тестирования. Когда мы первый раз врубили у себя эту оптимизацию, она реально сэкономила нам 20%. То есть эта цифра не с потолка, а из практики.
Если рассматривать эту закономерность дальше, то скорость тестов — это всегда функция от того, сколько у вас есть ресурсов и сколько у вас есть тестов. Дальше вы должны ее балансировать и как-то оптимизировать.
Потому что не все всегда одинаково. Предположим, к вам кто-то прибегает на ваш Continuous integration server и говорит, что нам нужно срочно запустить тесты — проверить фикс и сделать это как можно быстрее.
Вы можете пойти на поводу у этого человека и дать ему все возможные ресурсы для запуска тестов.
Правда может оказаться в том, что их фикс не очень важен по сравнению с текущим релизом, который должен выкатываться через два часа. Это первое.
А второе — тестов на самом деле не так много, как у вас ресурсов. То есть картинка, которую я показал раньше, где у вас 10 тестов и два ресурса, — это очень большое упрощение. Ресурсов может быть 200, а тестов — 10 тыс. И эта игра с тем, сколько кому дать ресурсов, начинает влиять на всех.
Чтобы правильно играть в эту игру, нужно всегда иметь ответы на два вопроса: сколько у вас ресурсов для запуска и сколько тестов.
Если вы будете достаточно долго думать над вопросом о том, сколько у вас ресурсов и сколько у вас тестов (особенно над последним), рано или поздно вы придете к мысли о том, что было бы неплохо парсить код ваших тестов и разбираться в том, что же в нем происходит:
Вам эта мысль может показаться безумной, но не гоните ее сразу. Все среды разработки уже делают это, чтобы показывать вам вот такие подсказки:
Причем они занимаются парсингом не только кода, но и всех зависимостей в нем.
Они умеют это делать. Более того, все среды разработки делают это хорошо, а некоторые даже поставляют библиотеки, которые позволяют решать такие задачи буквально в шесть строчек (по крайней мере, для Java).
В этих шести строках вы разбираете и полностью парсите какой-то кусок кода. Вы можете достать из него любую метаинформацию: сколько в нем полей, методов, конструкторов — чего угодно, в том числе тестов.
И имея все это в голове, мы создали сервис, который называется Berrimor.
BERRIMOR умеет говорить «овсянка, сэр!», а еще он умеет:
Я мог бы показать вам интерфейс BERRIMOR, но вы бы все равно ничего там равно бы. Вся его мощь кроется внутри API.
Социальный анализ кода
В 2010 году я читал лекции Сергея Архипенко по управлению программными проектами и мне запомнилась вот эта вот цитата:
«…реальность, которая заключена в особой специфике производства программ, по сравнению с любой другой производственной деятельностью, потому что то, что производят программисты – нематериально, это коллективные ментальные модели, записанные на языке программирования» (Сергей Архипенков, Лекции по управлению программными проектами, 2009).
Ключевое слово — коллективные. У людей есть почерк, но не у всех он хороший. У программистов тоже есть почерк (и также не всегда хороший). Между людьми существуют какие-то взаимосвязи: кто-то пишет фичу, кто-то ее патчит, кто-то ее чинит. Эти зависимости есть внутри каждого коллектива, внутри каждой команды разработки. И они влияют на качество того, что происходит в проекте.
Социальный анализ кода — нарождающаяся дисциплина. Я выделил три видео, которые есть в открытом доступе и могут помочь вам понять, что же это такое.
Социальный анализ кода позволяет:
Когда я что-то пишу и коммитчу, я указываю ссылку на тикет в Jira. В силу NDA я не могу показывать вам социальный анализ кода на примере репозиториев «Одноклассников». Я покажу на примере open source-проекта Kafka.
Итак, у меня есть (маленькое утилитное приложение), которое поднимает все коммиты в этом репозитории и разбирает все комментарии к ним, обеспечивая поиск по регулярному выражению Pattern.compile(«KAFKA-\\d+») коммитов, которые ссылаются на какой-то тикет.
В консоли видно, что коммитов всего 4246, а коммитов без такого упоминания — 1562. То есть точность анализа на треть меньше, чем хотелось бы.
Дальше мы поднимаем каждый коммит, составляем из него индекс — какие файлы в нем менялись (под какой тикет). Составляем все эти индексы в большой хэшмап: имя файла — список тикетов, по которым этот файл менялся. Вот как это выглядит:
Например, у нас есть файл KafkaApis и рядом огромный список issue, по которым он менялся (API меняется часто).
итоге мы получаем вот такой вывод:
Где мы пишем, какой процент изменений был в том или ином файле:
Например, для верхней строки общее количество тикетов, которые прошло в коммитах через этот файл, — 231, из них багов — 128 и, соответственно, 128 делим на 231 — получаем 55% — доля изменений. С большой вероятностью технический долг сосредоточен именно в этих файлах.
Итоги
Я вам показал шесть разных примеров. Это далеко не все, что существует. Но это значит, что белый ящик — это в первую очередь стратегия. Как вы ее будете реализовывать на вашем проекте — вам виднее. Надо думать, не надо бояться залезть в код. Там всегда лежит вся правда о вашем проекте. Поэтому читайте код, пишите код, вмешивайтесь в тот код, который пишут программисты.
Если тема тестирования и обработки ошибок вам так же близка, как и нам, наверняка вас заинтересуют вот эти доклады на нашей майской конференции Heisenbug 2018 Piter:



