Делай как вGoogle
![]()
|
Тестовые дублеры в Google Мы в Google видели бесчисленные примеры увеличения продуктивности инженеров и качества ПО за счет правильного использования тестовых дублеров. Изначально у нас почти не было руководств по эффективному применению тестовых дублеров, но по мере обнаружения общих паттернов и антипаттернов в базах кода многих команд мы разработали свои передовые практики в этой области. Один из уроков, которые мы усвоили, — чрезмерное использования фреймворков фиктивных объектов, которые позволяют легко создавать тестовые дублеры (мы обсудим такие фреймворки подробнее в этой главе), может быть опасно. Когда мы начали использовать фреймворки фиктивных объектов, они казались универсальным инструментом для создания узкоспециализированных тестов для изолированных фрагментов кода без конструирования зависимостей. Через несколько лет, накопив богатый опыт написания тестов, мы начали понимать стоимость таких тестов: легкие 258 Глава 13. Тестирование с дублерами в написании, они требовали постоянных усилий для поддержки и редко находили ошибки. В результате маятник качнулся в другую сторону: инженеры стали избегать фреймворков с фиктивными объектами, стараясь писать реалистичные тесты. Несмотря на то что согласие по практикам, обсуждаемым в этой главе, в целом достигнуто в Google, фактическое их применение в разных командах значительно различается. Это обусловлено противоречивостью мнений инженеров и инерцией в базах кода, не соответствующих этим практикам, и стремлением команд делать то, что быстрее всего окупится в краткосрочной перспективе. Базовые понятия Прежде чем углубиться в обсуждение приемов эффективного использования тесто- вых дублеров, обсудим некоторые базовые понятия, связанные с ними, и заложим фундамент для исследования передовых практик, рассмотренных в этой главе. Пример тестового дублера Представьте сайт электронной коммерции, который должен обрабатывать платежи по кредитным картам. Такой сайт может содержать код, примерно похожий на код в листинге 13.1. Листинг 13.1. Служба обработки кредитных карт class PaymentProcessor { private CreditCardService creditCardService; boolean makePayment(CreditCard creditCard, Money amount) { if (creditCard.isExpired()) { return false; } boolean success = creditCardService.chargeCreditCard(creditCard, amount); return success; } } Использовать в тесте реальную службу обработки кредитных карт невозмож- но (представьте, какую комиссию придется заплатить за транзакции, выполняемые в ходе тестирования!), зато можно использовать тестового дублера, эмулирующего поведение реальной системы. В листинге 13.2 демонстрируется чрезвычайно про- стой тестовый дублер. Листинг 13.2. Простейший тестовый дублер class TestDoubleCreditCardService implements CreditCardService { @Override public boolean chargeCreditCard(CreditCard creditCard, Money amount) { return true; } } Базовые понятия 259 Этот тестовый дублер не кажется очень полезным, но его использование в тесте позволит нам проверить логику в методе makePayment() . Для иллюстрации в лис- тинге 13.3 приводится тест, проверяющий поведение метода в случае предъявления кредитной карты с истекшим сроком действия (тест не зависит от поведения службы обработки кредитных карт). Листинг 13.3. Использование дублера в тесте @Test public void cardIsExpired_returnFalse() { boolean success = paymentProcessor.makePayment(EXPIRED_CARD, AMOUNT); assertThat(success).isFalse(); } Далее в этой главе мы обсудим особенности использования дублеров в более слож- ных ситуациях. Швы Код считается пригодным для тестирования ( https://oreil.ly/yssV2 ), если позволяет применить к нему юнит-тесты. Организация швов ( https://oreil.ly/pFSFf ) — это спо- соб подготовки кода к тестированию, добавление в него возможности применения тестовых дублеров. Швы дают возможность использовать при тестировании другие зависимости, отличные от используемых в продакшене. Внедрение зависимостей ( https://oreil.ly/og9p9 ) — распространенный метод орга- низации швов. Если класс использует внедрение зависимостей, любые другие не- обходимые ему классы (то есть зависимости) будут передаваться ему извне, а не создаваться непосредственно в нем, что позволит заменять эти зависимости в тестах. Листинг 13.4 иллюстрирует внедрение зависимости. Конструктор в этом примере не создает экземпляр CreditCardService , а получает его в виде параметра. Листинг 13.4. Внедрение зависимости class PaymentProcessor { private CreditCardService creditCardService; PaymentProcessor(CreditCardService creditCardService) { this.creditCardService = creditCardService; } } Код, вызывающий этот конструктор, должен создать соответствующий экземпляр CreditCardService . По аналогии с кодом, передающим реализацию CreditCardService , которая взаимодействует с внешним сервером, тест может передать тестового дублера. Листинг 13.5. Передача тестового дублера PaymentProcessor paymentProcessor = new PaymentProcessor(new TestDoubleCreditCardService()); 260 Глава 13. Тестирование с дублерами Чтобы уменьшить объем шаблонного кода, который приходится писать для вызова конструкторов вручную, используйте автоматизированные фреймворки внедрения зависимостей, позволяющие конструировать графы объектов, например Guice ( https://github.com/google/guice ) и Dagger ( https://google.github.io/dagger ) — интегри- рованные среды автоматического внедрения зависимостей в ПО на Java. В языках с динамической типизацией, таких как Python или JavaScript, можно дина- мически заменять отдельные функции или методы объекта. В этих языках внедре- ние зависимостей не так важно, потому что они позволяют использовать реальные реализации зависимостей в тестах, переопределяя только те функции или методы в зависимостях, которые неуместны для тестов. Написание кода, пригодного к тестированию, требует затрат, поэтому определите, насколько пригодным для тестирования должен быть ваш код, как можно раньше. Код, написанный без учета тестирования, как правило, приходится реорганизовывать или переписывать перед добавлением соответствующих тестов. Фреймворки фиктивных объектов Фреймворк фиктивных объектов (mocking framework) — это программная библиотека, которая упрощает создание тестовых дублеров. Он позволяет заменить объект его подделкой (моком) — тестовым дублером, поведение которого определяется непо- средственно в тесте. Использование фреймворков фиктивных объектов помогает сократить объем шаблонного кода, потому что избавляет вас от необходимости определять новый класс всякий раз, когда требуется использовать тестовый дублер. В листинге13.6 показано использование Mockito ( https://site.mockito.org ) — фрейм- ворка для Java. Mockito создает тестовый дублер для CreditCardService , который возвращает определенное значение. Листинг 13.6. Фреймворки фиктивных объектов class PaymentProcessorTest { PaymentProcessor paymentProcessor; // Тестовый дублер для CreditCardService создается единственной строкой кода @Mock CreditCardService mockCreditCardService; @Before public void setUp() { // Передать тестовый дублер в SUT paymentProcessor = new PaymentProcessor(mockCreditCardService); } @Test public void chargeCreditCardFails_returnFalse() { // Определить поведение для тестового дублера: вызов его метода // chargeCreditCard() всегда должен возвращать false. Использование // “any()” в аргументах метода требует от дублера возвращать // false независимо от значений аргументов Приемы использования тестовых дублеров 261 when(mockCreditCardService.chargeCreditCard(any(), any()) .thenReturn(false); boolean success = paymentProcessor.makePayment(CREDIT_CARD, AMOUNT); assertThat(success).isFalse(); } } Фреймворки фиктивных объектов существуют для большинства основных языков программирования. Мы в Google используем Mockito для Java, компонент googlemock в Googletest ( https://github.com/google/googletest ) для C++ и unittest.mock ( https://oreil. ly/clzvH ) для Python. Хотя фреймворки фиктивных объектов полезны, применять их следует с осторож- ностью, потому что злоупотребление ими часто усложняет поддержку кодовой базы. Некоторые из проблем, вызванных чрезмерным использованием таких фреймворков, мы рассмотрим в этой главе. Приемы использования тестовых дублеров Существуют три основных приема использования тестовых дублеров. Этот раздел кратко описывает их, чтобы вы могли получить общее представление об их особенно- стях и отличиях. А в последующих разделах мы более детально рассмотрим способы их эффективного применения. Инженер, знакомый с отличительными чертами этих приемов, сможет грамотно их применить, когда столкнется с необходимостью использовать тестовые дублеры. Имитации Имитация ( https://oreil.ly/rymnI ) — это легковесная реализация API, которая дей- ствует подобно реальной реализации, но не подходит для использования в продак- шене (например, база данных в памяти). В листинге 13.7 показано использование имитации. Листинг 13.7. Простая имитация // Имитации создаются быстро и просто AuthorizationService fakeAuthorizationService = new FakeAuthorizationService(); AccessManager accessManager = new AccessManager(fakeAuthorizationService): // Пользователь с неизвестным идентификатором не должен получить доступ assertFalse(accessManager.userHasAccess(USER_ID)); // После добавления идентификатора в службу авторизации пользователь должен // получить доступ fakeAuthorizationService .addAuthorizedUser(new User(USER_ID)); assertThat(accessManager.userHasAccess(USER_ID)).isTrue(); 262 Глава 13. Тестирование с дублерами Имитации часто идеально сочетаются с тестовыми дублерами. Но иногда для объ- екта, который необходимо использовать в тесте, нет подходящей имитации, а ее написание может быть затруднено из-за необходимости ее соответствия реальному объекту не только сейчас, но и в будущем. Заглушки Установка заглушки (стаба) ( https://oreil.ly/gmShS ) — это процесс присвоения функции некоторого поведения, которое иначе недоступно. С ее помощью вы указываете, что именно должна вернуть функция (то есть «заглушаете» фактические возвращаемые значения своими). Листинг13.8 иллюстрирует применение заглушки. Вызовы метода when(...).then- Return(...) из фреймворка Mockito определяют поведение метода lookupUser() Листинг 13.8. Заглушка // Передать тестовый дублер, созданый фреймворком фиктивных объектов AccessManager accessManager = new AccessManager(mockAuthorizationService): // Доступ должен быть закрыт, если функция поиска пользователя вернула null when(mockAuthorizationService.lookupUser(USER_ID)).thenReturn(null); assertThat(accessManager.userHasAccess(USER_ID)).isFalse(); // Доступ должен быть открыт, если функция поиска пользователя вернула // непустое значение when(mockAuthorizationService.lookupUser(USER_ID)).thenReturn(USER); assertThat(accessManager.userHasAccess(USER_ID)).isTrue(); Заглушки, как правило, реализуются с применением фреймворков фиктивных объ- ектов, чтобы уменьшить объем шаблонного кода, который пришлось бы написать, чтобы определить новые классы, возвращающие жестко заданные значения. Использование заглушек — простой и удобный метод, но он имеет свои ограничения, о которых мы поговорим в этой главе. Тестирование взаимодействий Тестирование взаимодействий ( https://oreil.ly/zGfFn ) — это проверка того, как вызы- вается функция, без фактического вызова ее реализации. Тест должен завершиться неудачей, если функция вызвана неправильно: вообще не была вызвана, вызывалась слишком много раз или была вызвана с неправильными аргументами. В листинге 13.9 показано тестирование взаимодействий. С помощью метода verify(...) из фреймворка Mockito проверяется, произошел ли ожидаемый вызов функции lookupUser() Подобно заглушкам, тестирование взаимодействий обычно реализуется с приме- нением фреймворков фиктивных объектов. Это сокращает объем шаблонного кода Реальные реализации 263 и избавляет вас от создания вручную новых классов, определяющих, как часто вы- зывается функция и какие аргументы ей переданы. Листинг 13.9. Тестирование взаимодействий // Передать тестовый дублер, созданный фреймворком фиктивных объектов AccessManager accessManager = new AccessManager(mockAuthorizationService); accessManager.userHasAccess(USER_ID); // Тест потерпит неудачу, если accessManager.userHasAccess(USER_ID) не // вызвал mockAuthorizationService.lookupUser(USER_ID) verify(mockAuthorizationService).lookupUser(USER_ID); Тестирование взаимодействий иногда называют имитированием ( https://oreil.ly/IfMoR ). Мы постараемся избегать этого термина в этой главе, чтобы не связывать имитации с фреймворками фиктивных объектов (подделок), которые можно использовать не только для тестирования взаимодействий, но и для создания заглушек. Как вы увидите далее в этой главе, тестирование взаимодействия полезно в опреде- ленных ситуациях, но по возможности его следует избегать, потому что его чрезмерное использование может привести к созданию хрупких тестов. Реальные реализации Тестовые дублеры могут быть эффективными инструментами тестирования, но мы больше предпочитаем использовать реальные реализации тестируемых зависимо- стей системы — то есть реализации, которые используются в самом коде, потому что тесты более достоверны, когда выполняют код, который будет действовать в продакшене. Преимущества использования реальных реализаций в тестовом коде открывались нам в течение многих лет, по мере того как мы наблюдали тенденцию захламления тестов повторяющимся кодом из-за чрезмерного использования фреймворков фик- тивных объектов, который не обновлялся синхронно с изменениями в реальной реализации и затруднял рефакторинг. Мы рассмотрим эту тему подробнее в этой главе. Предпочтительное использование реальных реализаций в тестах называется клас- сическим тестированием ( https://oreil.ly/OWw7h ). Соответственно в имитационном тестировании предпочтение отдается использованию фреймворков фиктивных объектов. Несмотря на то что некоторые инженеры в софтверной индустрии прак- тикуют имитационное тестирование (включая создателей первых фреймворков фиктивных объектов ( https://oreil.ly/_QWy7 )), мы в Google обнаружили, что этот стиль тестирования трудно масштабировать. При проектировании SUT инженеры долж- ны следовать строгим правилам ( http://jmock.org/oopsla2004.pdf ), а инженеры Google по умолчанию должны писать тестовый код, более подходящий для классического стиля тестирования. 264 Глава 13. Тестирование с дублерами ПРИМЕР: @DONOTMOCK Мы в Google видели достаточно тестов, которые чрезмерно полагаются на фреймворки фиктивных объектов, чтобы осознать необходимость создания аннотации @DoNotMock в Java, которая доступна как часть инструмента статического анализа ErrorProne ( https:// github.com/google/error-prone ). Эта аннотация позволяет владельцам API объявить, что «этот тип не должен замещаться фиктивной реализацией, потому что существуют лучшие альтернативы». Если инженер попытается использовать фреймворк фиктивных объектов, чтобы создать экземпляр класса или интерфейса, отмеченного аннотацией @DoNotMock, как показано в примере 13.10, он увидит сообщение об ошибке, предписывающее использовать более подходящую стратегию тестирования с применением реальной реализации или имитации. Эта аннотация чаще всего используется для объектов-значений, которые достаточно просты в использовании «как есть», а также для API, имеющих хорошо продуманные имитации. Листинг 13.10. Аннотация @DoNotMock @DoNotMock("Используйте SimpleQuery.create() вместо фиктивной реализации.") public abstract class Query { public abstract String getQueryValue(); } Почему это должно беспокоить владельцев API? Просто потому, что использование в те- стах фреймворков фиктивных объектов сильно ограничивает возможности владельца API вносить изменения в свою реализацию с течением времени. Как будет показано далее в этой главе, каждый раз, когда используется фреймворк фиктивных объектов, он дубли- рует поведение API. Решив изменить свой API, владелец может обнаружить, что поведение его API подделыва- ется в тысячах или даже десятках тысяч тестов во всей кодовой базе Google! Эти тестовые дублеры с большой вероятностью будут демонстрировать поведение, нарушающее контракт настоящего API, например возвращать null из метода, который никогда не может вернуть null. Если бы тесты использовали реальную реализацию или имитацию, владелец API смог бы внести изменения в свою реализацию, не исправляя предварительно тысячи тестов, затронутых этим изменением. Предпочтительность реализма перед изоляцией Использование реальных реализаций зависимостей делает тестирование системы более реалистичным, потому что тест выполняет код реальных реализаций. Напро- тив, тест, который использует тестовые дублеры, изолирует SUT от ее зависимостей, потому что тест не выполняет код фактических зависимостей SUT. Мы предпочитаем реалистичные тесты, потому что они дают больше уверенности в правильной работе SUT. Если юнит-тесты слишком сильно полагаются на тестовые дублеры, инженеру может потребоваться запустить интеграционные тесты или вруч- ную проверить правильность выполнения функции, чтобы достичь такого же уровня уверенности в тесте. Выполнение этих дополнительных задач замедлит разработку Реальные реализации 265 или, если инженеры вообще откажутся выполнять эти задачи из-за слишком боль- шой трудоемкости по сравнению с запуском юнит-тестов, в код проникнут ошибки. Замена всех зависимостей класса тестовыми дублерами приводит к тому, что тести- рованию подвергается только реализация, которую автор поместил непосредственно в класс. Однако хороший тест не должен зависеть от реализации — он должен быть написан с точки зрения тестируемого API, а не с точки зрения структуры реализации. Использование реальной реализации может вызвать сбой теста (или нескольких тестов), если в ней есть ошибка. И это хорошо! Тесты не должны завершаться успехом в подобных случаях, потому что такой «успех» означает, что тестируемый код не бу- дет работать правильно в продакшене. Но вооруженные хорошими инструментами, такими как система непрерывной интеграции, разработчики обычно легко находят изменение в реализации, вызвавшее сбой. |