Тест дизайн методом Interface — Model — State |
01.06.2023 00:00 |
Автор: Егор Романов (telegram) Yet another метод для разработки функциональных тест кейсов. Что будет, если отталкиваться от архитектурных схем тестируемой системы. ВступлениеЯ работаю в сфере автотестирования и уже не раз проходил через процесс вхождения в зрелые проекты. К сожалению, обычно это занимает больше времени, чем хотелось бы, так как тестировщик должен хорошо понимать бизнес требования, логику и техническое устройство тестируемых систем. На одном из собеседований мне задали довольно обыкновенный для области вопрос: «как ты пишешь тест кейсы, то есть какая у тебя методика по разработке тестов, покрывающих функциональность продукта?». Удивительно, но он поставил меня в тупик. Я читал и о традиционных подходах, и об интересном варианте из книги «Как тестируют в Google», но они не применялись мной в полной мере. Тогда я решил, что мне стоит сформулировать свою методологию и поделиться ей. Надеюсь, что вам будет полезна эта статья, даже если вы уже используете что-то подобное. ПроблематикаПредставьте, вы пришли на проект. Все улыбаются, все рады новому человеку и всегда готовы помочь. Вы открываете для начала документацию и диаграммы, чтобы понять, как тут всё работает. И получаете примерно вот такое: И собрав силы в кулак, вы начинаете разбираться в том, что скрыто за этим “magic”. При этом API системы иногда вообще не описывается, но даже если описано, сложно найти его на архитектурных схемах. То же самое касается бизнес сущностей, которыми оперирует ваш проект. Их найти на диаграммах вообще практически невозможно. Ну и напоследок, все мы знаем на практике, что компоненты могут находиться в разных состояниях (банально, доступен или нет), и модели тоже: наличие поля статус автоматически добавляют несколько состояний объекту. Перечисления этих самых состояний на схеме я не видел ни разу (такой вот я неудачник). Далее я расскажу, как я поступаю в таких ситуациях. В чем суть?Я отталкиваюсь от того, что любую тестируемую систему можно рассматривать как набор компонентов, из которых она состоит (это могут быть микросервисы, пакеты/модули, классы и т.п.), и моделей, которые в ней хранятся или ходят через неё. При этом:
Тогда если составить схему системы из компонентов, отметить для них модели, интерфейсы и состояния, можно, во-первых, увидеть как она работает и что может повлиять на ее работу. Во-вторых, используя комбинаторику, составить функциональный тест план для этой системы. Рассмотрим на примере:1. Составляем схемуДавайте представим, что нам нужно проверить компонент нашей системы, который отвечает за работу с заказами: “Order component”. ???? Для начала определимся с компонентами, которые взаимодействуют с нашим “Order component”. У него есть: база данных с заказами, он ходит во внешний “Payment provider” сервис, чтобы там обрабатывались платежи, а еще у нас есть шина событий, в которую наш компонент шлет эвенты о вызовах своего API.
???? Представим, какими моделями оперирует эта часть нашей системы: Вне всякого сомнения это сущность заказа — “Order”. Теперь немного призадумаемся, в нашем приложении заказ создать может только пользователь, значит в заказе хранится информация о нем, и API нашего сервиса может использовать тоже пользователь, таким образом вторая модель — “User”. Идем дальше, в заказах содержится также информация о его составе, то есть о продуктах, то есть третья модель — “Product”. Разбираемся, в каких состояниях могут находиться наши модели:
???? Определим, какими интерфейсами обладают компоненты системы: 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 заказа, который в свою очередь тоже содержит пользователя, создавшего заказ. Таким образом:
Вроде готово, окинем все взглядом еще раз, и… Мы замечаем, что заказ содержит продукты, а у продуктов тоже могут быть разные состояния. А что если мы пытаемся оформить заказ, в котором содержатся продукты, которые уже удалены? А если количество продукта в заказе больше, чем осталось у нас? Пожалуй стоит добавить еще один компонент на нашу схему и добавить шаг аналогичный сверке с базой, только мы будем отправлять запрос на уменьшение кол-ва остатков продукта, если этот вызов завершится неудачей, то мы опять же должны пойти по негативному сценарию. Так мы обнаружили, что мы упустили целое взаимодействие, но грамотно описывая схему, мы смогли заметить это на раннем этапе — составлении тесткейсов. Вот теперь получше. Если все проверки прошли, то двигаемся дальше. А дальше мы должны вызвать сторонний сервис для оплаты заказа по карте. Стоп. У нас же есть еще два состояния у пользователя: есть или нет информации о карте. То есть это тоже нужно провалидировать и если данных карты нет, то пойти опять по негативному кейсу. Пока всё, с валидацией покончено, идем к следующему шагу. Дополнительно отметим для негативных сценариев: необходимо проверить, что дальнейшие шаги не выполнены, последующие вызовы не были исполнены, в базе данных ничего не поменялось. ???? Вызов стороннего сервиса для совершения платежа. Начинаем с состояний, в которых может находиться сервис. Он может быть доступен или нет (на самом деле он может еще отвечать медленно, этот вариант тоже стоило бы рассмотреть, но мы опустим, чтобы ускориться).
Если эта часть прошла успешно, то следующие действия должны быть выполнены параллельно, нам в действительности не так важен порядок здесь, но за одним нюансом. Наш компонент должен добиться успеха от следующих шагов, потому что пользователь уже оплатил заказ. ???? Обновление статуса заказа. Тестируемый компонент должен сходить в базу и обновить состояние заказа на Paid. Здесь снова нужно разбираться, как себя поведет Order component, в зависимости от состояния базы данных. Что будет если она доступна? Если нет? Еще раз замечу, что во всех негативных кейсах, которые мы рассматривали до этого, нам стоит проверять, что тестируемое приложение не пыталось обновить заказ. ???? Отправка события в шину данных о том, произошла оплата успешно или нет. Отправку события об ошибке оплаты мы уже не раз проверяли в рассмотренных ранее негативных кейсах. Отмечу здесь только то, что нам стоит проверять, что именно в этих случаях отправляется в эвенте orderPayFailed. Корректна ли там ошибка? Если же все прошло успешно, то мы должны отправить событие об успешной оплате orderPaid и проверить его тело. ???? Возвращение ответа: оплата успешна или нет. Происходить должна в любом случае. Опять же стоит проверять тексты и коды ошибок в любом из кейсов. ИтогМы получили набор сценариев, которые могут произойти при работе с тестируемым компонентом. Мы описали основные бизнес сегменты нашей системы. Это модели, интерфейс компонентов, состояния, в которых могут быть компоненты и модели. А также взаимодействия, происходящие при использовании методов из тестируемого интерфейса. Затем можно заметить, что мы использовали комбинаторику для моделей, интерфейсов, компонентов и их состояний, чтобы сформировать набор тест кейсов. За счет такого подхода мы получили достаточно обширный тест план. Так как для всего мы составляли схемы и использовали их при изучении функциональности, мы смогли заметить, что изначально пропустили пару моментов. Когда пользуюсьМеня этот подход особенно выручал, когда приходилось тестировать какие-то важные куски функциональностей, в которых хочется быть крайне уверенным. Но на самом деле, я обращаюсь к нему всегда: и когда нужно досконально проверить фичу, и когда нужно разобраться в чем-то новом. На самом деле, даже когда нужно быстро что-то проверить, тоже использую, но схемы накидываю очень абстрактно на бумажку или вообще держу в голове. Юниты, интеграционные, е2е?Рассмотренный пример касался компонентного и интеграционного тестирования в большей степени. Но мы бы легко смогли превратить кейсы, например, в end-2-end. Для схемы, нарисованной выше, нам необходимо было бы рассмотреть, кто подписан на события orderPaid и orderPayFailed. Разобраться, что происходит с подписчиками при получении этих эвентов. У нас бы добавились, например, снятие с резерва или наоборот списание продуктов в product component, отправка уведомлений в компоненте нотификаций с использованием внешних сервисов и так далее. А для того, чтобы переделать набор в юнит-тесты, необходимо только все вокруг замокать и застабить (хотя говорят в юнитах стабов нет) ???? Кроме того, в качестве схемы может быть uml диаграмма или набор экранов с приложеньки. Как рисовать это сугубо дело вкуса, удобства и уровня тестируемого компонента. ЗаключениеСильно запутал?:) Даже если так, то сложной эта система кажется только на первый взгляд. Можно перечитать еще раз и одновременно положить рядом какой-нибудь кусочек своей системы. Разложить все точно так же для него. Я уверен, что всё пойдет как по маслу. В данном случае практика легче теории ???? Надеюсь, что моя статья окажется вам полезной! Делитесь тем, как вы подходите к тест дизайну. Также буду рад услышать ваше мнение о том, как работает мой мыслительный процесс. Пишите мне и давайте обсуждать эту тему в Всем удачи! |