Что такое заглушка в тестировании
Перейти к содержимому

Что такое заглушка в тестировании

  • автор:

Что такое заглушка в тестировании

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

Рисунок 1: Тестирование реализованного компонента

Эти другие компоненты могут вызывать сложности при тестировании:

  1. Они могут быть еще не реализованы.
  2. В них могут быть ошибки, нарушающие работу тестов, причем на выяснение того, что ошибка была вызвана не тестируемым, а зависимым компонентом, может уйти много времени и усилий.
  3. Компоненты могут затруднять тестирование тогда, когда это действительно надо. Например, если компонент — коммерческая база данных, у вас может оказаться недостаточно лицензий для всех пользователей. Другой пример: компонент может представлять собой аппаратное обеспечение, доступное только в определенные промежутки времени в определенной лаборатории.
  4. Компоненты могут настолько замедлить тестирование, что станет невозможно выполнять тесты достаточно часто. Например, инициализация базы данных может занимать пять минут.
  5. Может быть сложно поставить компонент в условия, при которых он выдаст ожидаемый от него результат. Например, перед вами может стоять задача протестировать обработку сообщения «диск переполнен» для всех методов, осуществляющих запись на диск. Как гарантировать, что диск будет заполнен именно на момент вызова нужного метода?

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

Рисунок 2: Тестирование компонента с заглушкой вместо компонента, от которого он зависит

У заглушек есть два недостатка.

  1. Заглушки могут быть дорогостоящими (это особенно актуально для эмуляторов). Поскольку заглушки сами по себе представляют программное обеспечение, они нуждаются в обслуживании.
  2. Заглушки могут маскировать ошибки. Например, предположим, что ваш компонент пользуется тригонометрическими функциями, но к настоящему моменту соответствующая библиотека еще не разработана. Вы пользуетесь тремя тестами для вычисления синуса трех углов: 10 градусов, 45 градусов и 90 градусов. Вы находите нужные значения с помощью калькулятора и строите заглушку функции синуса, которая возвращает на эти входные параметры значения 0,173648178, 0,707106781 и 1,0, соответственно. Все прекрасно работает до момента интеграции компонента в готовую библиотеку, в которой функция синуса принимает на входе значения в радианах и возвращает значения -0,544021111, 0,850903525 и 0,893996664. В итоге вы обнаруживаете ошибку в коде позже и потратив больше усилий, чем хотелось бы.

Заглушки и практика разработки программного обеспечения

Иногда заглушки создаются только потому, что реальный компонент еще не доступен на момент тестирования. Во всех остальных случаях заглушки следует сохранять после завершения разработки. Скорее всего, тесты, поддерживаемые заглушками, будут важны при обслуживании продукта. Поэтому качество заглушек должно быть выше, чем качество кода на выброс. Хотя к качеству заглушек не предъявляются настолько жесткие требования, как к коду продукта (например, большинство заглушек не нуждается в собственных тестах), в дальнейшем некоторые из них придется обслуживать в процессе изменения продукта. Если обслуживание будет требовать слишком больших усилий, заглушки будут выброшены, и инвестиции в них будут потеряны.

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

Сценарий 1: база данных используется как для тестирования, так и для работы. . Существование базы данных не нужно скрывать от компонента. Можно даже инициализировать компонент с именем базы данных:

public Component(  String databaseURL) < try < databaseConnection = DriverManager.getConnection(databaseURL); . >catch (SQLException e) <. >>

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

public String get(String key) < try < Statement stmt = databaseConnection.createStatement(); ResultSet rs = stmt.executeQuery( "SELECT value FROM Table1 WHERE key HTMLMarkup" endspan --> 
Сценарий 2: в целях тестирования база данных заменена заглушкой. Код компонента должен быть независим от того, с чем он работает: с реальной базой данных или с заглушкой. Поэтому в коде должны применяться методы абстрактного интерфейса:
  interface KeyValuePairs < String  get(String key); void  put(String key, String value); >

В тестах пары из ключей и значений KeyValuePairs будут реализованы с помощью какой-нибудь простой конструкции, например таблицы:

  class FakeDatabase implements KeyValuePairs < Hashtable table = new Hashtable(); public String  get(String key) public void  put(String key, String value) >

За исключением периода тестирования, компонент будет пользоваться адаптером, преобразующим вызовы KeyValuePairs в операторы SQL:

  class DatabaseAdapter implements KeyValuePairs < private Connection databaseConnection; public DatabaseAdapter(String databaseURL) < try < databaseConnection = DriverManager.getConnection(databaseURL); . >catch (SQLException e) <. >> public String  get(String key) < try < Statement stmt = databaseConnection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT value FROM Table1 WHERE key #ff0000">put(String key, String value) <. >>

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

class Component <  public Component(String databaseURL) < this.valueStash = new DatabaseAdapter(databaseURL); >// For testing.  protected Component(KeyValuePairs valueStash) < this.valueStash = valueStash; >>

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

Дополнительная информация

  • Endo-Testing: Unit testing with Mock Objects, "eXtreme Programming and Flexible Processes in Software Engineering - XP2000". © 2000 Tim Mackinnon, Steve Freeman, Philip Craig.
  • (Загрузить Adobe Reader)
  • Инструмент: Rational QualityArchitect
  • Инструмент: Rational Test RealTime

© Copyright IBM Corp. 1987, 2006. Все права защищены..

Заглушка

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

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

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

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

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

Если вы используете моки, то вы хоть что-то тестируете?

Было ли у вас ощущение, что ради тестирования вы делаете код труднее для чтения? Допустим, у вас есть код, который ещё не тестировался. У него есть ряд побочных эффектов, и вас просят сначала прогнать тесты. Вы начинаете следовать советам вроде передачи глобальных переменных в виде параметров или извлечения проблемных побочных эффектов, чтобы сделать вместо них заглушки в тестах.

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

Вы останавливаетесь и задумываетесь: «Допустимо ли менять сигнатуры кода ради тестирования? Тестирую ли я реальный код или совершенно другой класс, в котором не происходит то, что нужно?» Перед вами может возникнуть дилемма. Вы уверены, что стоит и дальше придерживаться этого подхода? Или это потеря времени?

Вопрос на миллион: для устаревшего кода нужно писать модульные тесты или интеграционные?

Парадокс

Вы могли оказаться в одной из этих ситуаций:

Не знаю, как вам, а мне эти ситуации оооооочень близки. Они смешные, поскольку правдивые (и это неприятно).

Вам говорят, что нужно делать заглушки для проблемных зависимостей и писать модульные тесты. Но в какой-то момент возникает ощущение, что в ваших тестах растёт слепая зона. Какие у вас есть гарантии, что код будет вести себя корректно при использовании настоящей БД в бою? Не станет ли у вас ещё больше проблем, если вы интегрируетесь с настоящей базой данных?

Я тоже сталкивался с этой дилеммой! Долгое время я продвигал Testing Pyramid. Там утверждают, что нужно писать больше модульных тестов, потому что они быстрые и более надёжные, чем интеграционные тесты.

Также я много занимался фронтендом. В последние годы набирает популярность мантра «Пишите тесты. Не слишком много. В основном, интеграционные», которую продвигает Кент Доддс. Он очень умён и является авторитетом в тестировании фронтенд-приложений. Есть даже инструменты вроде Cypress для тестирования большинства веб-приложений с помощью сквозных тестов! Ещё ближе к пользователю!

Как вы разрешите этот парадокс? Как будет правильно? Стоит ли делать заглушки, чтобы тесты были быстрыми и надёжными? Или лучше писать интеграционные тесты, которые менее надёжны, но вылавливают больше проблем?

Прежде чем мы продолжим, позвольте сказать: замечательно, что перед вами встала эта дилемма. Она представляется вам проблемой, но это означает, что вы движетесь в правильном направлении! Это естественное препятствие на пути, и вы его преодолеете. У этой загадки есть решение.

Практичный взгляд на тестирование

Я на некоторое время погрузился в эту проблему и попробовал разные подходы. Решение пришло после памятной дискуссии с J. B. Rainsberger.

Я понял, что никакого парадокса нет. Есть разные определения «модуля».

Мне кажется, слово «модуль» вносит путаницу. У людей разное понимание, что это такое. Обычно новички в тестировании считают модулем функцию или метод. Потом они понимают, что это может быть целый класс или модуль. Однако истина, как и многое другое в жизни, зависит от ситуации.

Изолированные тесты

Я считаю, что деление на «модульные» и «интеграционные» тесты недостаточно понятно. Такая категоризация приводит к проблемам и спорам, хотя цель у нас одна: облегчить изменение ПО с помощью быстрой и точной обратной связи, когда вы что-то ломаете.

Поэтому я предпочитаю говорить об «изолированных» тестах.

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

Мой ответ: изолированные от того, что сложно тестировать.

Что сложно тестировать

Случайность, время, сеть, файловую систему, базы данных и т.д.

Всё это я называю «инфраструктурой», а остальное — «доменом». Сочетание домена и инфраструктуры выглядит очень полезным и практичным взглядом на проектирование ПО. И не только для меня. Такое разделение заложено в основу многих удобных для сопровождения архитектур, таких как гексагональная архитектура (Hexagonal Architecture) (известная как «порты и адаптеры» (Ports and Adapters)), луковая архитектура (Onion Architecture), чистая архитектура (Clean Architecture), функциональное ядро / императивная оболочка (Functional Core / Imperative Shell) и т.д.

Всё это нюансы, которые говорят об одном: домен и инфраструктуру лучше разделять. Эту идею продвигает и функциональное программирование: изолированные побочные эффекты на периметре вашей системы. Побочные эффекты — это инфраструктура. А её трудно тестировать.

Да, но чем это поможет?

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

Изолированный от инфраструктуры домен тестировать легко. Вам не нужно делать заглушки для других объектов домена. Можно использовать тот же код, что используется в проде. Нужно «всего лишь» избавиться от взаимодействия с инфраструктурой.

«Но ведь в коде инфраструктуры могут быть баги!»

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

Суть вот в чём: у вас должна быть возможность тестировать все виды поведения вашего приложения без сохранения данных в PostgreSQL. Нужна in-memory БД. Вам всё ещё нужно проверять работоспособность интеграции с PostgreSQL, но для этого достаточно всего несколько тестов, а это заметное отличие!

Всё это была теория, давайте вернёмся к реальности: у нас есть код, который ещё не протестирован.

Возвращаемся к устаревшему коду

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

Вам нужно стремиться извлечь инфраструктурный код из доменного.

Затем сделайте так, чтобы инфраструктура зависела от домена, а не наоборот. В бизнес-логике должно быть как можно меньше инфраструктуры. У вас получатся быстрые, надёжные, изолированные тесты, покрывающие большую часть логики приложения, и несколько интеграционных тестов для проверки работы инфраструктурного кода с реальной базой данных, внешним API, системными часами и т.д.

Останутся ли у вас заглушки? И да, и нет.

У вас будут альтернативные реализации инфраструктуры, поэтому вы всё ещё сможете исполнять production-логику без использования настоящей БД.

Как узнать, что реализация БД работает корректно? Напишите несколько интеграционных тестов. Как раз столько, чтобы хватило для проверки корректности поведения. J. B. Rainsberger называет это «Тестами контракта». Думаю, лучше всего пояснить на этом примере.

Можете следовать этому рецепту

Вот как можно работать с устаревшим кодом:

  1. Используйте методику Extend & Override для ликвидации неприятных зависимостей. Это хороший первый шаг. Вырежьте инфраструктурную часть из оставшегося кода, чтобы можно было начать писать тесты.
  2. Инвертируйте зависимость между кодом и извлечённой инфраструктурой. Для этого используйте внедрение зависимостей (Dependency Injection). Придётся принять архитектурные решения: можно ли сгруппировать некоторые извлечённые методы в согласованный класс? Например, сгруппировать то, что относится к сохранению и извлечению сохранённых данных. Это ваш инфраструктурный адаптер.
  3. Если вы пишете на статически типизированном языке, то извлеките интерфейс из созданного класса. Это простой и безопасный рефакторинг. Он позволит завершить инверсию зависимости: сделает код зависимым от интерфейса, а не от конкретной извлечённой реализации.
  4. В тестах сделайте фальшивую реализацию этого интерфейса. Скорее всего, она будет хранить информацию в памяти. Она должна воспроизводить такой же интерфейс и вести себя так же, как и боевая реализация, которую вы вытащили.
  5. Напишите тест контракта. Это гарантирует, что фальшивая и боевая реализации ведут себя так, как вы ожидаете! Тест контракта проверит корректность работы интерфейса. Поскольку вы прогоните одинаковые серии тестов на обеих реализациях, вы будете знать, что можно доверять своим изолированным тестам. Вы в безопасности! Отличное руководство для начинающих.
  6. Сделайте так, чтобы интерфейс выражал потребности бизнеса, а не подробности реализации. Рекомендую развивать архитектуру в этом направлении. Но это приходит с практикой и не должно быть помехой.

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

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

Всё будет отлично

У меня ушло несколько лет на размышления. И я всё ещё читаю, слушаю и пробую этот подход в разных ситуациях (пока что работает).

Но всё это важно: тестирование, архитектура и устаревший код. Все эти навыки взаимосвязаны. Практикуясь в одном, вы растёте и в другом.

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

И последнее: вам не обязательно сразу выполнять все 6 этапов. Можете заниматься этим итеративно. Нужно верить в этот процесс (вот почему он хорошо подходит для практики), но он работает. Если вас беспокоит потеря времени на излишнее количество заглушек, то это потому, что вы всё ещё на первом этапе. Идите дальше! Заглушек станет меньше, а у вас появится больше уверенности в точности тестов.

Инфраструктурные адаптеры + тесты контрактов = недостающие части пилы для устаревшего кода.

Возвращайтесь к работе и продолжайте писать эти тесты. А затем идите дальше.

  • Блог компании VK
  • Тестирование IT-систем
  • PHP
  • Проектирование и рефакторинг
  • Тестирование веб-сервисов

Тестовые двойники — имитации, подставные объекты и заглушки

Разбираемся в различиях между тремя этими видами тестовых двойников.

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

Хотя тестовые двойники бывают разных вариантов (Жерар Мезарос (Gerard Meszaros) представил пять типов в этой статье), люди склонны использовать термин “Mock” для обозначения различных видов тестовых двойников. Непонимание и смешение реализаций тестовых двойников влияют на дизайн тестов и повышают их хрупкость, препятствуя плавный рефакторинг.

В этой статье я опишу три варианта реализации тестовых двойников: Fake (имитация, поддельный объект), Stub (заглушка) и Mock (подставной объект), а также предоставлю примеры, когда их использовать.

Fake (имитация)

Имитация (или поддельные объекты) — это объекты, имеющие рабочие реализации, но не такие, как у настоящих рабочих объектов. Обычно они идут коротким путём и имеют упрощённую версию реального объекта.

В качестве примера может быть реализация в оперативной памяти объектов доступа к данным (Data Access Object) или репозиторий (Repository). Реализация поддельных объектов не будет привлекать базу данных, но будет использовать простую коллекцию для хранения данных. Это позволяет нам выполнять интеграционный тест сервисов без участия базы данных и выполнения тем самым трудоёмких запросов.

Мы вряд ли захотим закрывать настоящие двери, чтобы проверить, работает ли метод обеспечения защиты, верно? Вместо этого мы создаём подставные объекты для дверей и окон и используем их в тестовом коде.

После выполнения метода securityOn, подставные объекты окон и дверей зарегистрируют все взаимодействия с ними (читай — вызовы их). Это позволяет нам проверить, что окна и двери были проинструктированы о закрытии. Это всё, что нам нужно проверить, с точки зрения SecurityCentral.

Вы можете спросить, как мы можем утверждать, будут ли закрыты настоящие окна и двери, если мы тестируем на подставных объектах? Ответ в том, что мы не можем. Это не ответственность SecurityCentral. Это ответственность только двери и окна, чтобы они закрыли себя, когда получают верный сигнал об этом. Мы можем протестировать их независимо в разных модульных тестах.

Дальнейшее чтение

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *