Pull to refresh

Правильное Unit тестирование: декомпозиция тест кейсов в теории и на практике

Level of difficultyEasy
Reading time7 min
Views4.2K

Предисловие

Мы продолжаем наш цикл статей о тестировании. Ранее мы узнали о распространённости юнит тестирования в среде разработчиков, а также о том, стоит ли нам, разработчикам, тестировать свой код (спойлер: всё же скорее стоит). Сегодня же мы затронем несколько более прикладную часть грамотного процесса тестирования, а именно создание тест кейсов. Первоначально выделение кейсов может показаться разработчику тривиальной задачей, но, как мы скоро увидим, данный процесс можно определить некоторым набором правил.

Что это такое и зачем это надо

Для начала давайте представим себе какой-то компонент, который нам было бы интересно тестировать. Хрестоматийным примером в сегодняшнем контексте будет являться проверка паролей пользователя при регистрации. Если кто-то когда-либо пытался написать свой собственный фреймворк авторизации, то он может представлять себе сложность проблемы. Но я предлагаю упростить всё до одного лишь только "проверятора" паролей. Возьмём какую-нибудь стандартную конфигурацию: в требованиях у нас есть максимальная и минимальная длина, разрешённые и запрещённые символы, а также строгое требование иметь по символу из каждого множества (заглавные буквы, маленькие буквы, цифры, специальные символы, китайские иероглифы, т. д.). Причём физически данный "проверятор" может представлять из себя класс или функцию в нашем любимом языке программирования, либо может быть даже уже готовую формочку. Но это не столь важно, поэтому давайте представим его как некую неизвестную нам функцию. У функции есть только условные входы (строка пароля) и условные выходы (ок, не ок, почему), а о её внутреннем устройстве нам известно немногое. Как вы могли догадаться, речь идёт о т. н. "Black Box" подходе.

Теперь, держа в уме этот конкретный пример, мы можем попробовать разобрать его на кейсы. Сначала это кажется простым: нам нужно показать, что тестируемая система выдаёт ожидаемый результат. А это означает тест кейс на "положительных" данных. Иными словами, дав "правильный" пароль на вход, мы ожидали бы в качестве ответа абстрактный "ок". А надо ли писать дальше? Допустим, что мы хотим также протестировать, что "проверятор" ответит нам "не ок" на неправильный пароль. Но ведь пароль может быть неправильным на нескольких условиях. Надо ли проверять для каждого из них? И если да, то как детально?

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

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

Методология

Классы эквивалентности

Вернёмся к примеру с паролями.

Первоначально сложность с построением тест кейсов на данном примере может создаваться именно ввиду большого множества состояний, которые может принимать пароль - правильных и неправильных. Тем не менее, как таковой "проверятор" паролей имеет конечный и весьма ограниченный набор поведений. А именно: наша система может выдать в качестве ответа "ок" если пароль правильный. На каждую потенциальную причину, по которой пароль может быть неправильный, система может отдать в качестве ответа "не ок" вместе с сообщением об ошибке.

Если рассматривать все возможные входные и выходные параметры через призму этого конечного набора поведений, то мы увидим, что каждый вариант пароля соответствует ровно одному варианту поведения "проверятора". Таким образом, общее множество всех возможных входных и выходных значений можно поделить на подмножества по принципу принадлежности к какому-то одному варианту поведения "проверятора". Примером такого подмножества может служить, скажем, отсутствие цифры в пароле - в таком случае мы ожидали бы результат "не ок" от проверятора с сообщением, что в пароле не хватает хотя бы одной цифры. Но так как мы теперь знаем, что все варианты входных/выходных значений внутри каждого подмножества вызовут одинаковое поведение у тестируемой программы, то тогда для проверки этого самого варианта поведения программы нам достаточно выполнить её только лишь на одном значении из этого множества, ведь для остальных значений мы ожидаем ровно такое же поведение. Здесь мы делаем важное допущение, что мы с какой-то точностью уверены, что выполнен будет один и тот же набор машинных инструкций в каждом сценарии внутри подмножества.

Таким образом, на каждый вариант поведения "проверятора" есть множество всех вызывающих его входных параметров. И нам нужен только один тест кейс, чтобы этот вариант поведения проверить. Каждое из таких подмножеств принято называть классом эквивалентности, а сам метод подбора тест кейсов таким образом называется equivalence class partitioning. Термин происходит из математики, где, как можно догадаться, обозначает он что-то примерно похожее на описанное выше. Но нам это сейчас не интересно, а интересен наш "проверятор". Применив к нему нашу теорию, мы получим следующие тест кейсы:

Правильная длина

Имеет заглавные буквы

Имеет строчные буквы

Имеет цифры

Имеет спец. символ

Имеет иероглиф

Не имеет неразрешённых символов

Ожидаемый вывод

1

Да

Да

Да

Да

Да

Да

Да

Ок

2

Нет

Да

Да

Да

Да

Да

Да

Не Ок, причина

3

Да

Нет

Да

Да

Да

Да

Да

Не Ок, причина

4

Да

Да

Нет

Да

Да

Да

Да

Не Ок, причина

5

Да

Да

Да

Нет

Да

Да

Да

Не Ок, причина

6

Да

Да

Да

Да

Нет

Да

Да

Не Ок, причина

7

Да

Да

Да

Да

Да

Нет

Да

Не Ок, причина

8

Да

Да

Да

Да

Да

Да

Нет

Не Ок, причина

Как видите, всё довольно просто.

Комбинаторика классов

Тут сразу может возникнуть вопрос: а почему нельзя совмещать вместе два класса с ошибками? Скажем, у нас две любые ошибки в одном пароле. Например, и длина не та, и цифр нету. Об этом данная теория немного умалчивает. Объяснить это, тем не менее, можно следующим образом. Мы подразумеваем, что причина об ошибке будет только одна. Может быть где-то в самом коде у нас проверяются вся ошибки по очереди, и после первой сработавшей проверки мы не проверяем дальше. Ну, то есть всё равно такой ввод будет принадлежать только к одному классу ошибки. Всё немного сложнее, когда мы можем комбинировать ошибки и писать в ответе обо всех ошибках сразу. К счастью для нас, в данном случае проверить нужно только один компонент системы, который мы не проверили ранее - комбинаторику ошибок. Проверить это можно ровно одним тест кейсом, где ошибок более одной. Это может быть тест кейс на два нарушенных условия или на все сразу - не суть. Это всё равно будет, как можно догадаться, один класс.

Правильная длина

Имеет заглавные буквы

Имеет строчные буквы

Имеет цифры

Имеет спец. символ

Имеет иероглиф

Не имеет неразрешённых символов

Ожидаемый вывод

9

Нет

Нет

Нет

Нет

Нет

Нет

Нет

Не Ок, всё плохо

Множество правильных классов

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

Давайте попробуем исходить из нашего понимания о том, зачем мы пишем тест кейсы. Одной из задачей является защититься от регрессий, или неправильного поведения после изменения условий. А это значит, что если у нас есть какой-то условно конечный список правильных значений, то протестировать имеет смысл на них всех. Мы хотим это сделать для того, чтобы убедиться, что этот список внезапно просто так не поменялся.

Наиболее простым примером такого поведения является тест, где параметром является Enum. Например, у нас есть enum class PASSWORD_COMPLEXITY { INSECURE, AVERAGE, COMPLEX, VERY_COMPLEX }. Допустим, что у нас есть какой-то предикат, задача которого просто разрешить все пароли сложностью COMPLEX или сложнее. Мы получаем следующие тест кейсы:

Значение Переменной

Ответ предиката

1

INSECURE

false

2

AVERAGE

false

3

COMPLEX

true

4

VERY_COMPLEX

true

Это весьма примитивный пример, но в реальном коде такое может встречаться весьма часто. К счастью, на написание тестов на данную функцию уйдёт несколько минут реального времени, ведь все современные библиотеки тестирования имеют возможность делать параметризованные тест кейсы. Да и обнаружить тест кейсы тут можно без особого труда. Не будем сейчас останавливаться на этом надолго.

Метод интервалов

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

Рассмотрим из примера с паролем отдельно требование по длине. Допустим, что пароль должен быть длинной в 10 символов или более, но строго меньше 20. Мы теперь можем представить, длина пароля - это числовой дискретный параметр, который можно нанести на условную ось. В таком случае на данной оси будут следующие возможные интервалы:

(0, 10)

[10, 20)

(20, MAX_LENGTH)

не ок

ок

не ок

Мы видим, как из одного неправильного класса эквивалентности мы получили два. Но, как вы могли догадаться, и это ещё не всё. Интервальный метод призывает нас обратить особое внимание на граничные значения. Это предлагается сделать с целью убедиться, что границы на самом деле проведены верно. А работает это условно так:

  • Если граничная точка X у нас "закрашенная" (inclusive), то необходимо написать тест кейсы для значений X, X-1 и X+1. Например, так как у нас "закрашенная" точка 10, то нам понадобятся тест кейсы для значений 9, 10 и 11.

  • Если граничная точка (X) у нас выколотая, то написать надо тест кейсы для значений X и X-1 или X+1, зависимо от того, какое из них попадает в другой интервал. Для точки 20 это будут 20 и 19. На самом деле, канонично иногда призывают протестировать ещё и для точек 21 и 18. Это мотивируется тем, что (19, 20) у нас на самом деле и есть тестируемая точка, а ещё нужно по тесту внутри каждого интервала. Это может быть избыточно, но я бы этому не препятствовал.

И да, отдельно обращаю внимание на случай, когда числа у нас условно недискретные. Например, если тип данных у нас float или double. В таком случае, когда мы берём точки вроде X-1 или X+1 нам достаточно взять просто что-то приближенное.

Вернёмся к нашей первоначальной таблице с матрицей тестирования "проверятора" паролей. Уберём из неё все остальные параметры, кроме длины. Они нам сейчас не нужны. Ранее, тестов по длине у нас было всего два. Теперь же, тесты у нас будут следующие:

Правильная длина

Ожидаемый вывод

1

Нет (9)

Не ок, причина

2

Да (10)

Ок

3

Да (11)

Ок

4

Да (19)

Ок

5

Нет (20)

Не ок, причина

Выводы

Мы ознакомились с тем, как можно теоритечески обосновать декомпозицию тест кейсов. Подобной методологии тестирования хватает лично мне, чтобы тестировать большинство написанного мною в моей деятельности кода. Единственное, как вы могли заметить, подобное несколько плохо применимо на более сложных кейсах. Скажем, когда у нас несколько независимых параметров. На них я хотел бы остановиться уже в отдельной статье.

Tags:
Hubs:
Total votes 7: ↑7 and ↓0+7
Comments3

Articles