Тест дизайн методом Interface — Model — State

Egor Romanov
10 min readFeb 14, 2021

Yet another метод для разработки функциональных тест кейсов. Что будет, если отталкиваться от архитектурных схем тестируемой системы.

English Version — https://egor-romanov.medium.com/test-design-using-the-interface-model-state-method-d1bcb053bcbc

Вступление.

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

На одном из собеседований мне задали довольно обыкновенный для области вопрос: «как ты пишешь тест кейсы, то есть какая у тебя методика по разработке тестов, покрывающих функциональность продукта?». Удивительно, но он поставил меня в тупик. Я читал и о традиционных подходах, и об интересном варианте из книги «Как тестируют в Google», но они не применялись мной в полной мере.

Тогда я решил, что мне стоит сформулировать свою методологию и поделиться ей. Надеюсь, что вам будет полезна эта статья, даже если вы уже используете что-то подобное.

Моя прекрасная рыжая кошка для настроения😺

Проблематика

Представьте, вы пришли на проект. Все улыбаются, все рады новому человеку и всегда готовы помочь. Вы открываете для начала документацию и диаграммы, чтобы понять, как тут всё работает. И получаете примерно вот такое:

И собрав силы в кулак, вы начинаете разбираться в том, что скрыто за этим “magic”. При этом API системы иногда вообще не описывается, но даже если описано, сложно найти его на архитектурных схемах. То же самое касается бизнес сущностей, которыми оперирует ваш проект. Их найти на диаграммах вообще практически невозможно. Ну и напоследок, все мы знаем на практике, что компоненты могут находиться в разных состояниях (банально, доступен или нет), и модели тоже: наличие поля статус автоматически добавляют несколько состояний объекту. Перечисления этих самых состояний на схеме я не видел ни разу (такой вот я неудачник).

Далее я расскажу, как я поступаю в таких ситуациях.

В чем суть?

Я отталкиваюсь от того, что любую тестируемую систему можно рассматривать как набор компонентов, из которых она состоит (это могут быть микросервисы, пакеты/модули, классы и т.п.), и моделей, которые в ней хранятся или ходят через неё. При этом:

  1. Эти компоненты оперируют некоторым набором моделей. Например, пользователь или заказ.
  2. У компонентов и у моделей могут быть состояния. Например, пользователь может быть залогинен, удален и т.п. Какой-то из компонентов может быть доступен, недоступен, может быть «медленным».
  3. У компонентов есть некоторый интерфейс (gui, rest api, grpc, подписки на события, cli, набор публичных методов и т.п.), этот интерфейс состоит из методов (или подписок на события), их параметров и результатов вызова: возвращаемых ответов, дополнительных вызовов, отправленных эвентов, изменений состояния моделей или самого компонента.

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

Рассмотрим на примере:

1. Составляем схему

Давайте представим, что нам нужно проверить компонент нашей системы, который отвечает за работу с заказами: “Order component”.

🟢 Для начала определимся с компонентами, которые взаимодействуют с нашим “Order component”. У него есть: база данных с заказами, он ходит во внешний “Payment provider” сервис, чтобы там обрабатывались платежи, а еще у нас есть шина событий, в которую наш компонент шлет эвенты о вызовах своего API.

Все наши компоненты могут быть в одном из состояний: доступен, недоступен, медленный (запросы обрабатываются долго). В том числе и шина событий.

🟡 Представим, какими моделями оперирует эта часть нашей системы: Вне всякого сомнения это сущность заказа — “Order”. Теперь немного призадумаемся, в нашем приложении заказ создать может только пользователь, значит в заказе хранится информация о нем, и API нашего сервиса может использовать тоже пользователь, таким образом вторая модель — “User”. Идем дальше, в заказах содержится также информация о его составе, то есть о продуктах, то есть третья модель — “Product”.

  • Разбираемся, в каких состояниях могут находиться наши модели:

1. User может быть в одном из следующих: не существует, в текущий момент залогинен, не залогинен, удален. А также если он существует, то у него может быть одно из 2 состояний: указаны данные банковской карты или нет.
2. Product тоже может быть в одном из состояний: не существует, имеет количество в доступности (от 0 и больше), удален.
3. Order может быть в состояниях: не существует, создан, удален, оплачен и закрыт.

🔴 Определим, какими интерфейсами обладают компоненты системы:

1. Тестируемый сервис “Order”:

  • Мы можем создавать заказ;
  • Отменять или удалять заказ;
  • Оплачивать заказ;
  • Закрывать его.

Для простоты давайте оставим для разбора только метод “payOrder”. Он принимает аргумент ID заказа. Но с точки зрения бизнес логики, вызвать его может только пользователь. Плюс к этому заказ за своим ID содержит информацию о продукте и пользователе хозяине заказа. Итого: полный список бизнесовых аргументов для метода payOrder выглядит так:

  • Пользователь, который вызвал метод;
  • Заказ;
  • Продукты из заказа;
  • Продукты из заказа;

Теперь разберемся, что происходит, когда метод payOrder вызван:

  • Валидация аргументов;
  • Вызов стороннего сервиса для совершения платежа и ожидание ответа;
  • Обновление статуса заказа, если он успешно оплачен;
  • Отправка события в шину данных о том, произошла оплата успешно или нет;
  • Возвращение ответа: оплата успешна или нет.

Важный момент: мы как тестировщики достоверно не знаем порядок этих событий. Это необходимо помнить и учитывать.

Модели, с которыми работает сервис, исходя из аргументов тестируемого метода — это User, Order, Product.

2. База данных “Orders DB”:
Поддерживает CRUDL для заказов. Заказы содержат в себе информацию о продуктах и пользователях — хозяевах. Ответы на вызовы — это успех или неудача, а также отсутствие ответа. То есть модели те же — User, Order, Product.

3. Сторонняя интеграция — “Payment provider”:
Используется из тестируемого компонента Orders только при вызове метода payOrder. Имеет важный для нас API в виде одного метода payByCard, который принимает номер заказа, его сумму и данные банковской карты пользователя. Ответы на вызовы — это тоже успех или неудача, а также отсутствие ответа. То есть, используемые им в нашем бизнес понимании модели — это User и Order.

4. А также у нас есть “Event Bus”:
Туда Order component шлет события о вызовах его API. Для вызовов метода payOrder — это будут два события в зависимости от статуса: orderPaid и orderPayFailed. Эти события содержат информацию о заказе и о причине неудачи для orderPayFailed.

Фух, вроде готово. Теперь у нас перед глазами есть наглядная схема того, как работает наша система, какие у неё есть компоненты, как они связаны, какими бизнес сущностями они манипулируют, какой интерфейс имеют. Осталось только добавить схему того, что происходит при вызове методов тестируемого компонента.

2. Тест дизайн

Мы уже рассмотрели, какие действия должны происходить, когда мы вызываем тестируемый метод, давайте вспомним и разберем порядок этих действий:

🟢 Валидация аргументов.
Пойдем по порядку. Мы передаем метаинформацию о пользователе, который вызвал метод. Мы передаем id заказа, который в свою очередь тоже содержит пользователя, создавшего заказ. Таким образом:

  • Наш метод должен возвращать ошибку и пытаться отправить событие orderPayFailed, если юзер, который вызвал метод, невалидный, то есть, к примеру, удален;
  • Далее должна произойти валидация заказа, чтобы собрать информацию о нем, компоненту необходимо сходить в базу данных. Тут мы понимаем, что дальше пути два по состояниям базы:
    - База данных доступна
    - Или недоступна
    Если недоступна, то мы должны вернуть ошибку и отправить эвент в шину о том, что оплата обломалась, так как БД недоступна (а если на секунду призадуматься, то еще должны отстреливать алерты и метрики об этом, потому что это кажется ненормальным, таким образом мы заметили еще один момент, который стоило бы проверить).
  • Данные о заказе собрали, сверяем их, заказ должен существовать, это во-первых. Во-вторых, он должен быть только в одном состоянии — Created, иначе опять же высылаем эвент о неудаче и возвращаем ошибку. И наконец, пользователь, вызвавший метод, должен совпадать с тем, кто заказ создал, иначе ошибка, событие о неудаче.

Вроде готово, окинем все взглядом еще раз, и… Мы замечаем, что заказ содержит продукты, а у продуктов тоже могут быть разные состояния. А что если мы пытаемся оформить заказ, в котором содержатся продукты, которые уже удалены? А если количество продукта в заказе больше, чем осталось у нас? Пожалуй стоит добавить еще один компонент на нашу схему и добавить шаг аналогичный сверке с базой, только мы будем отправлять запрос на уменьшение кол-ва остатков продукта, если этот вызов завершится неудачей, то мы опять же должны пойти по негативному сценарию. Так мы обнаружили, что мы упустили целое взаимодействие, но грамотно описывая схему, мы смогли заметить это на раннем этапе — составлении тесткейсов.

Вот теперь получше. Если все проверки прошли, то двигаемся дальше. А дальше мы должны вызвать сторонний сервис для оплаты заказа по карте. Стоп. У нас же есть еще два состояния у пользователя: есть или нет информации о карте. То есть это тоже нужно провалидировать и если данных карты нет, то пойти опять по негативному кейсу.

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

🟡 Вызов стороннего сервиса для совершения платежа.
Начинаем с состояний, в которых может находиться сервис. Он может быть доступен или нет (на самом деле он может еще отвечать медленно, этот вариант тоже стоило бы рассмотреть, но мы опустим, чтобы ускориться).

  • Если сервис недоступен, то отрабатывает негативная ветка с возвращением ошибки и отправкой события orderPayFailed. Опять же в реальной жизни стоило бы добавить еще проверку алертинга и метрик, и вообще добавить на схему отвечающие за это компоненты.
  • Если же сервис доступен, то мы должны проверить два пути его ответов — это success и fail. Если на запрос мы получаем ошибку, то идет негативная ветка, если success, то двигаемся дальше. Стоит подумать, в каких случаях нам может вернуться негативный ответ. Это может быть невалидная карта, проблема с суммой оплаты, дефекты на стороне payment provider. Эти кейсы нужно проверить.
  • Третий вариант, который тоже возможен, — отсутствие ответа. Необходимо проверить, как на эту ситуацию будет реагировать наша система и непосредственно тестируемый Order component.

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

🟠 Обновление статуса заказа.
Тестируемый компонент должен сходить в базу и обновить состояние заказа на Paid. Здесь снова нужно разбираться, как себя поведет Order component, в зависимости от состояния базы данных. Что будет если она доступна? Если нет? Еще раз замечу, что во всех негативных кейсах, которые мы рассматривали до этого, нам стоит проверять, что тестируемое приложение не пыталось обновить заказ.

🔴 Отправка события в шину данных о том, произошла оплата успешно или нет.
Отправку события об ошибке оплаты мы уже не раз проверяли в рассмотренных ранее негативных кейсах. Отмечу здесь только то, что нам стоит проверять, что именно в этих случаях отправляется в эвенте orderPayFailed. Корректна ли там ошибка? Если же все прошло успешно, то мы должны отправить событие об успешной оплате orderPaid и проверить его тело.
Опять же все эти кейсы: и положительные, и отрицательны должны учитывать как хороший сценарий, где шина событий доступна, так и плохой, где она недоступна или медленная. Есть ли у нас буфер на такие случаи, не потеряем ли мы эвенты?

🟣 Возвращение ответа: оплата успешна или нет.
Происходить должна в любом случае. Опять же стоит проверять тексты и коды ошибок в любом из кейсов.

Итог

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

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

Когда пользуюсь

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

Но на самом деле, я обращаюсь к нему всегда: и когда нужно досконально проверить фичу, и когда нужно разобраться в чем-то новом. На самом деле, даже когда нужно быстро что-то проверить, тоже использую, но схемы накидываю очень абстрактно на бумажку или вообще держу в голове.

Юниты, интеграционные, е2е?

Рассмотренный пример касался компонентного и интеграционного тестирования в большей степени.

Но мы бы легко смогли превратить кейсы, например, в end-2-end. Для схемы, нарисованной выше, нам необходимо было бы рассмотреть, кто подписан на события orderPaid и orderPayFailed. Разобраться, что происходит с подписчиками при получении этих эвентов. У нас бы добавились, например, снятие с резерва или наоборот списание продуктов в product component, отправка уведомлений в компоненте нотификаций с использованием внешних сервисов и так далее.

А для того, чтобы переделать набор в юнит-тесты, необходимо только все вокруг замокать и застабить (хотя говорят в юнитах стабов нет) 👍

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

Заключение

Сильно запутал?:) Даже если так, то сложной эта система кажется только на первый взгляд. Можно перечитать еще раз и одновременно положить рядом какой-нибудь кусочек своей системы. Разложить все точно так же для него. Я уверен, что всё пойдет как по маслу. В данном случае практика легче теории 😉

Надеюсь, что моя статья окажется вам полезной!

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

Всем удачи!

--

--