| Контрактное тестирование: что (не) тестировать – часть 1 |
| 22.12.2025 00:00 |
|
Недавно я начал работать с новым клиентом, который уже какое-то время внедряет контрактное тестирование и решил, что ему не помешает помощь со стороны. Я недавно навестил его, и, чтобы максимально эффективно использовать наше совместное время (нужно было ехать в другой город, а я мог отлучиться лишь на один день), он заранее прислал мне длинный список вопросов, составленный разными командами разработчиков за последние несколько месяцев. Эти вопросы охватывали разные аспекты контрактного тестирования, но некоторые из них вдохновили меня написать эту статью — именно потому, что я видел, как многие команды сталкиваются с очень похожими вопросами. Сейчас я работаю сразу с тремя компаниями, помогая им внедрять контрактное тестирование, и подобные вопросы возникали у всех команд. На самом деле, к написанию меня подтолкнули не сами вопросы, а то, что стоит за ними — а именно эта проблема: «Где заканчивается контрактное тестирование и начинается тестирование реализации сервиса-поставщика?» Я стараюсь напоминать об этом каждый раз — и во время докладов о контрактном тестировании, и в рамках обучающих курсов и воркшопов: контрактное тестирование — это техника, которая дополняет функциональное и приёмочное тестирование сервисов-поставщиков, но не заменяет его. Если уж контрактное тестирование что-то и заменяет, то это часть ваших интеграционных и end-to-end тестов, а не тестов для конкретного сервиса. Сформулируем это по-другому. Если функциональное тестирование отдельного сервиса направлено на то, чтобы ответить на вопрос: «Соответствует ли поведение этого сервиса ожиданиям, сформулированным командой, которая его разрабатывает?», то контрактное тестирование, особенно в варианте с потребительской инициативой (consumer-driven), стремится ответить на вопрос: «Соответствует ли поставщик ожиданиям, сформулированным его потребителями?» Однако грань между этими двумя вопросами — и, соответственно, граница, где заканчивается один тип тестирования и начинается другой — не всегда бывает чёткой. Давайте рассмотрим пример, чтобы разобраться, что, на мой взгляд, должно, а что не должно входить в контрактный тест. Контекст статьиВ этой статье я сосредоточусь исключительно на интеграциях, основанных на HTTP. В следующей публикации я рассмотрю эту же проблему применительно к системам, использующим обмен сообщениями. Хотя между этими двумя типами интеграций много общего в вопросе «что следует (или не следует) включать в контрактный тест», в системах обмена сообщениями есть и уникальные особенности, заслуживающие отдельного внимания. Клиент, о котором я упоминал в начале статьи, практически повсеместно использует интеграции на основе обмена сообщениями, так что текущая публикация может быть не особенно актуальна для этого случая. Тем не менее, описываемая здесь проблема справедлива для любого типа интеграции. Также стоит отметить, что в статье рассматривается подход контрактного тестирования, ориентированного на потребителя (consumer-driven). В следующей статье я также планирую взглянуть на вопрос с точки зрения двустороннего контрактного тестирования. Чтобы разобраться с вопросом, который мы рассматриваем здесь, давайте рассмотрим пример HTTP-интеграции между сервисом Customer (потребитель) и сервисом Address (поставщик). Начнем с операции, при которой сервис Customer получает данные об адресе из сервиса Address, используя HTTP GET-запрос к эндпойнту: /address/{id}
где {id} — это уникальный идентификатор конкретного адреса в базе данных поставщика. Предположим также, что сервис использует UUID в качестве значения идентификатора ресурса, и что пример JSON-представления адреса, возвращаемого поставщиком, выглядит так: {
Наконец, допустим, что наш потребитель сейчас работает только с адресами из США, и на самом деле не использует название города. В США связь между почтовым индексом и городом является однозначной, так что с точки зрения потребителя передача и названия города, и индекса — это избыточность. Сценарии для проверкиВ данной ситуации можно выделить несколько возможных сценариев, включая:
Могут существовать и другие сценарии, например, зависящие от наличия в запросе корректных данных для аутентификации, но сейчас остановимся на этих трех. Обратите внимание: при написании контрактных тестов важно с самого начала продумывать возможные сценарии, поскольку ошибочные сценарии (с кодами ответа в диапазоне 400) так же важны для покрытия, как и успешные (с кодами в диапазоне 200). Сценарий «счастливого пути» и ожидания относительно формы тела ответаНачнем со сценария HTTP 200. В контракте мы должны указывать только те свойства ответа, которые действительно необходимы потребителю для обработки, но не более того. Почему? Потому что чем больше требований вы предъявляете как потребитель и чем они специфичнее, тем выше шанс, что поставщик не сможет им соответствовать — а это приведет к ложноположительным результатам контрактных тестов. Кроме того, если несколько потребителей зависят от одного и того же поставщика, то чрезмерные требования одного из них увеличивают вероятность конфликта интересов между потребителями. Другими словами: просите только то, без чего не обойтись, и ничего лишнего. Переформулирую: «просите только необходимое и достаточное». Итак, для сценария HTTP 200, в контракте потребителя мы можем указать, что ожидаем:
Что не следует указывать в контракте — это то, какой именно будет ответ для конкретного ID адреса. То есть, в контракте стоит указать, что:
Но не стоит указывать, что, если мы запрашиваем адрес по определенному ID, то:
Почему этого делать не стоит? Потому что в таком случае мы уже тестируем не интеграцию между потребителем и поставщиком (например, «могу ли я распарсить корректно возвращённое представление адреса?»), а реализацию поставщика (например, «возвращает ли поставщик ожидаемые данные для конкретного адреса?»). А это уже не цель контрактного теста. Да, технически можно использовать контрактные тесты и, например, Pact, для таких проверок, но тогда тесты станут очень хрупкими, поскольку между кодом потребителя и тестируемым поведением поставщика возникнет жесткая связка. А что насчёт поля country в ответе? Как упоминалось выше, наш клиент в настоящее время работает только с адресами США. Это означает, что теоретически мы можем задать более строгие ожидания относительно поля country, указав в контракте, что значение этого поля всегда должно быть "United States". Однако есть веские причины не делать этого и вместо этого проверять лишь то, что поле country содержит строку:
Первая причина — хороший пример того, что граница между тестированием интеграции потребителя и поставщика и тестированием реализации поставщика не всегда однозначна. Именно поэтому, как уже говорилось ранее, лучше просить как можно меньше, а не как можно больше. Теперь рассмотрим поле updated. В этом случае как потребители, мы действительно хотим быть чуть строже и не просто проверять тип элемента (строка). Хоть смысла проверять точную дату и время последнего изменения конкретного адреса и нет, нам важно убедиться, что значение можно распарсить. Иными словами, дата и время должны быть возвращены в формате, который мы можем корректно обработать. Один из способов добиться этого — использование регулярных выражений, и такие инструменты контрактного тестирования, как Pact, позволяют указать ожидаемый формат поля с помощью regex. Это отличный вариант для подобных ситуаций. То же самое касается поля zipCode. Мы можем потребовать, чтобы значение индекса было пятизначным числом (пусть оно и приходит в виде строки), например, с помощью выражения \d{5}. Но здесь мы уже стоим на грани между контрактным тестом и проверкой реализации. Такой подход может привести к ложноположительным результатам. Если вы всё же решите задавать строгий формат индекса, будет разумно указать соответствующее состояние поставщика, требующее от него возвращать адрес США. Минус такого подхода — это рост количества очень специфичных состояний поставщика, что может усложнить внедрение контрактного тестирования на стороне поставщика, поскольку им придётся поддерживать больше вариантов состояний в своих верификационных тестах. Важно помнить: этот поставщик, скорее всего, обслуживает не только вас. Да, это уже звучит как заезженная пластинка, но повторим ещё раз: запрашивайте только то, без чего точно не обойтись. Наконец, как уже упоминалось, сервис потребителя не использует значение поля city, так как полностью полагается на индекс при обработке адреса. Поэтому будет хорошей практикой не указывать это поле в ожиданиях потребителя. В последний раз повторим: запрашивайте только то, без чего вы не можете обойтись. Обработка ошибокТеперь, когда мы рассмотрели, как должен выглядеть контракт в случае успешного ответа (HTTP 200), можно проделать ту же работу для сценариев с кодами HTTP 404 и HTTP 400. В этих случаях, скорее всего, сервис потребителя не ожидает от поставщика адресов никаких данных в теле ответа, и единственное требование потребителя — это получить корректный HTTP-статус:
Для операций, отличных от HTTP GET, подход остаётся тем же: определить возможные сценарии на основе HTTP-статусов. Затем для каждого сценария обдумать, что именно необходимо от поставщика, чтобы корректно обработать ответ. Осторожно относитесь к границе между интеграционным контрактным тестированием и тестированием реализации поставщика. Завершение и «продолжение следует…»В этом материале я постарался прояснить границу между тестированием интеграции потребителя и поставщика и тестированием реализации поставщика для HTTP-интеграций. Как мы увидели, эта граница не всегда очевидна, и за ней нужно внимательно следить при написании и запуске контрактных интеграционных тестов. В конце концов, наша цель — избежать множества хрупких контрактных тестов, которые ломаются при малейшем изменении реализации на стороне поставщика и требуют дополнительного анализа, обсуждений и доработки. Для этого существуют модульные и системные тесты самого поставщика. В следующей статье я рассмотрю эту же тему, но применительно к не-HTTP интеграциям, поскольку их контрактное тестирование отличается от HTTP-интеграций. Как и обещал, я также рассмотрю эту тему с позиции двухстороннего контрактного тестирования. |