Делай как вGoogle
Скачать 5.77 Mb.
|
Пример злоупотребления заглушками В листинге 13.13 приводится тест, злоупотребляющий заглушками. Листинг 13.13. Злоупотребление заглушками @Test public void creditCardIsCharged() { // Передать в вызов конструктора тестовые дублеры, // созданные фреймворком фиктивных объектов paymentProcessor = new PaymentProcessor(mockCreditCardServer, mockTransactionProcessor); // Настроить заглушки для этих тестовых дублеров when(mockCreditCardServer.isServerAvailable()).thenReturn(true); when(mockTransactionProcessor.beginTransaction()).thenReturn(transaction); when(mockCreditCardServer.initTransaction(transaction)).thenReturn(true); when(mockCreditCardServer.pay(transaction, creditCard, 500)) .thenReturn(false); when(mockTransactionProcessor.endTransaction()).thenReturn(true); // Вызвать SUT paymentProcessor.processPayment(creditCard, Money.dollars(500)); // Здесь невозможно определить, действительно ли метод pay() выполнил // транзакцию; тест может проверить только, вызывался ли метод pay() verify(mockCreditCardServer).pay(transaction, creditCard, 500); } В листинге 13.14 представлена улучшенная версия того же теста, не использую- щая заглушки. Обратите внимание, что тест получился короче и в нем нет деталей реализации (например, порядка использования процессора транзакций). Никаких специальных настроек не потребовалось, потому что сервер обработки кредитных карт знает, как себя вести. Листинг 13.14. Реорганизованная версия теста без заглушек @Test public void creditCardIsCharged() { paymentProcessor = new PaymentProcessor(creditCardServer, transactionProcessor); Тестирование взаимодействий 273 // Вызвать SUT paymentProcessor.processPayment(creditCard, Money.dollars(500)); // Запросить состояние сервера кредитных карт, чтобы определить, // был ли выполнен платеж assertThat(creditCardServer.getMostRecentCharge(creditCard)) .isEqualTo(500); } Конечно, крайне нежелательно, чтобы такой тест взаимодействовал с внешним сервером кредитных карт, поэтому здесь использована имитация сервера. Если готовой имитации сервера нет, можно использовать его реальную реализацию, на- строенную на взаимодействие с герметичным сервером кредитных карт, правда, это увеличит время выполнения тестов. (С герметичными серверами мы познакомимся в следующей главе.) Когда использование заглушек оправданно? Заглушки лучше использовать, когда нужно получить от функции определенное воз- вращаемое значение, чтобы привести SUT в определенное состояние, как показано в листинге 13.12, который требует, чтобы SUT вернула непустой список транзакций. Поскольку поведение функции определяется прямо в тесте, заглушка может эмули- ровать широкий спектр возвращаемых значений или ошибок, которые невозможно получить при использовании реальной реализации или имитации. Чтобы более ясно отразить свою цель, каждая заглушка должна иметь прямую связь с утверждениями в тесте. Соответственно, тест должен использовать минимум за- глушек, потому что большое количество заглушек в тесте может ухудшить его яс- ность и будет свидетельствовать о злоупотреблении заглушками или о том, что SUT слишком сложна и ее следует реорганизовать. Обратите внимание, что даже в случаях, когда применение заглушек оправданно, предпочтительнее все же использовать реальные реализации или имитации, потому что они не раскрывают детали реализации и дают больше гарантий правильности кода по сравнению с заглушками. Но заглушки остаются разумной методикой те- стирования, если используются ограниченно и не усложняют тесты. Тестирование взаимодействий Как обсуждалось выше в этой главе, тестирование взаимодействий помогает про- верить, как вызывается функция, без фактического вызова ее реализации. Применение фреймворков фиктивных объектов упрощает тестирование взаимо- действий. Однако чтобы тесты были полезными, удобочитаемыми и устойчивыми к изменениям, тестирование взаимодействий следует использовать только при явной необходимости. 274 Глава 13. Тестирование с дублерами Тестируйте состояние, а не взаимодействия Вместо взаимодействий лучше тестировать состояние ( https://oreil.ly/k3hSR ). При тестировании состояния вы вызываете SUT и проверяете, вернула ли она правильное значение или правильно ли она изменила какое-то другое состояние. В листинге 13.15 показан тест, проверяющий состояние. Листинг 13.15. Тестирование состояния @Test public void sortNumbers() { NumberSorter numberSorter = new NumberSorter(quicksort, bubbleSort); // Вызвать SUT List sortedList = numberSorter.sortNumbers(newList(3, 1, 2)); // Проверить, отсортирован ли список. Алгоритм сортировки не важен, // если возвращен правильный результат assertThat(sortedList).isEqualTo(newList(1, 2, 3)); } В листинге 13.16 похожим образом протестированы взаимодействия. Обратите внимание, что в этом тесте невозможно определить, действительно ли числа отсор- тированы, потому что тестовые дублеры не знают, как выполнять сортировку, — они могут только сообщить, что SUT пыталась отсортировать числа. Листинг 13.16. Тестирование взаимодействий @Test public void sortNumbers_quicksortIsUsed() { // Передать в вызов конструктора тестовые дублеры, // созданные фреймворком фиктивных объектов NumberSorter numberSorter = new NumberSorter(mockQuicksort, mockBubbleSort); // Вызвать SUT numberSorter.sortNumbers(newList(3, 1, 2)); // Убедиться, что numberSorter.sortNumbers() использовала алгоритм // быстрой сортировки (quicksort). Тест потерпит неудачу, если // mockQuicksort.sort() не будет вызвана (например, если будет // использована mockBubbleSort) или вызвана с неверными аргументами verify(mockQuicksort).sort(newList(3, 1, 2)); } Мы в Google заметили, что тестирование состояния более масштабируемо: оно повы- шает надежность тестов, упрощает внесение изменений в код и легко поддерживается с течением времени. Основная проблема тестирования взаимодействий состоит в том, что оно не дает уверенности, что SUT работает должным образом, и может только подтвердить, что определенные функции вызываются как следует. Это вынуждает нас строить пред- Тестирование взаимодействий 275 положения о поведении кода, например: «Если вызывается database.save(item) , то мы предполагаем, что элемент item сохранен в базе данных». Тестирование состояния для нас предпочтительнее, потому что оно фактически проверяет сделанное предпо- ложение (например, сохраняет элемент в базе данных и затем получает его, чтобы узнать, действительно ли элемент сохранен). Еще один недостаток тестирования взаимодействий — использование деталей реа- лизации SUT. Например, чтобы убедиться, что функция была вызвана, вы должны сообщить тесту, какую именно функцию система должна вызвать. Так же как в случае с заглушками, дополнительный код сделает тесты хрупкими из-за проникновения в тесты деталей реализации кода. Некоторые инженеры в Google в шутку называ- ют тесты, которые злоупотребляют тестированием взаимодействий, детекторами изменений ( https://oreil.ly/zkMDu ), потому что они терпят неудачу в ответ на любое изменение в коде, даже если поведение SUT остается неизменным. Когда тестирование взаимодействий оправданно? Вот несколько случаев, когда тестирование взаимодействий оправданно: y Иногда выполнить тестирование состояния невозможно, потому нельзя использо- вать реальную реализацию или имитацию (например, если реальная реализация слишком медленная или имитация отсутствует). В таких случаях как запасной вариант можно выполнить тестирование взаимодействий и убедиться, что опре- деленные функции действительно вызываются. Это не идеальное решение, но оно хотя бы дает некоторую уверенность в том, что SUT работает должным образом. y Различия в количестве или порядке вызовов функций могут стать причиной неже- лательного поведения. В этом случае может пригодиться тестирование взаимодей- ствий, потому что с помощью тестирования состояния трудно проверить такое пове- дение. Например, если ожидается, что функция кеширования сократит количество обращений к базе данных, тестирование взаимодействий покажет, действительно ли число обращений к базе данных не превысило ожидаемого количества. Если в тесте использовать фреймворк Mockito, его код будет выглядеть примерно так: verify(databaseReader, atMostOnce()).selectRecords(); Тестирование взаимодействий не может полностью заменить тестирование состояния. Если невозможно выполнить тестирование состояния в юнит-тесте, настоятельно реко- мендуем дополнить свой набор тестами с более широким охватом, выполняющими тести- рование состояния. Например, если у вас есть юнит-тест, проверяющий использование базы данных посредством тестирования взаимодействий, добавьте к нему интеграцион- ный тест, который проверит состояние на реальной базе данных. Расширенное тестирова- ние является важной стратегией снижения рисков, и мы обсудим ее в следующей главе. Передовые практики тестирования взаимодействий Следование перечисленным ниже практикам тестирования взаимодействий помогает ослабить влияние вышеупомянутых недостатков. 276 Глава 13. Тестирование с дублерами Используйте тестирование взаимодействий только для функций, изменяющих состояние Функции зависимостей, вызываемые SUT, относятся к одной из двух категорий: Изменяющие состояние Функции с побочными эффектами, наблюдаемыми за пределами SUT. Например: sendEmail() , saveRecord() , logAccess() Не изменяющие состояние Функции, не имеющие побочных эффектов, которые возвращают информа- цию о мире за пределами SUT, но ничего не изменяют. Например: getUser() , findResults() , readFile() Как правило, тестирование взаимодействий следует выполнять только для функций, изменяющих состояние. Тестирование взаимодействий для функций, не изменяю- щих состояние, обычно излишне, поскольку SUT будет использовать возвращаемое значение функции для выполнения другого действия, которое можно проверить. Само взаимодействие не влияет на правильность кода, если оно не имеет побочных эффектов. Тестирование взаимодействий для функций, не изменяющих состояние, делает тесты хрупкими, потому что эти тесты нужно обновлять при каждом изменении паттерна взаимодействий, и менее удобочитаемыми, потому что дополнительные инструкции затрудняют определение, какие инструкции важны для проверки правильности кода. Тестирование взаимодействий, изменяющих состояние, напротив, намного полезнее, потому что помогает проверить, что делает код для изменения состояния где-то еще. Листинг 13.17 демонстрирует тестирование взаимодействий для функций, изменя- ющих и не изменяющих состояние. Листинг 13.17. Взаимодействия, изменяющие и не изменяющие состояние @Test public void grantUserPermission() { UserAuthorizer userAuthorizer = new UserAuthorizer(mockUserService, mockPermissionDatabase); when(mockPermissionService.getPermission(FAKE_USER)).thenReturn(EMPTY); // Вызвать SUT userAuthorizer.grantPermission(USER_ACCESS); // addPermission() изменяет состояние, поэтому для нее разумно выполнить // тестирование взаимодействий, чтобы убедиться, что она вызывается verify(mockPermissionDatabase).addPermission(FAKE_USER, USER_ACCESS); // getPermission() не изменяет состояние, поэтому эту строку кода можно // опустить. Вывод о бессмысленности тестирования взаимодействий можно // сделать по наличию заглушки для getPermission() выше в этом тесте verify(mockPermissionDatabase).getPermission(FAKE_USER); } Тестирование взаимодействий 277 Избегайте чрезмерного уточнения деталей В главе 12 мы обсудили, почему полезнее тестировать поведение, а не методы. Мы выяснили, что тест должен проверять только один аспект поведения метода или класса, а не несколько сразу. Тот же принцип желательно применять при выполнении тестирования взаимодей- ствий и избегать чрезмерного уточнения таких деталей, как проверяемые функции и аргументы, чтобы повысить ясность и краткость кода, а также его устойчивость к изменениям в поведении SUT, которое выходит за рамки отдельного теста. Листинг 13.18 иллюстрирует тестирование взаимодействий с чрезмерным уточ- нением деталей. Целью теста является проверка присутствия имени пользователя в тексте приветствия. Этот тест провалится, если изменится поведение, не связанное с полученным текстом. Листинг 13.18. Тест взаимодействий с чрезмерным уточнением @Test public void displayGreeting_renderUserName() { when(mockUserService.getUserName()).thenReturn("Fake User"); userGreeter.displayGreeting(); // Вызов SUT // Тест провалится, если изменится какой-либо из аргументов setText() verify(userPrompt).setText("Fake User", "Good morning!", "Version 2.1"); // Тест провалится, если setIcon() не будет вызвана, несмотря на то что // это поведение не имеет прямого отношения к тесту, потому что // не связано с проверкой имени пользователя verify(userPrompt).setIcon(IMAGE_SUNSHINE); } В листинге 13.19 показан подход к тестированию взаимодействий с более осторож- ным уточнением деталей в отношении аргументов и функций. Тестируемое поведе- ние разделено на отдельные тесты, и каждый тест проверяет минимум из того, что необходимо проверить, чтобы убедиться в правильности тестируемого поведения. Листинг 13.19. Тесты взаимодействий без чрезмерного уточнения деталей @Test public void displayGreeting_renderUserName() { when(mockUserService.getUserName()).thenReturn("Fake User"); userGreeter.displayGreeting(); // Вызов SUT verify(userPrompter).setText(eq("Fake User"), any(), any()); } @Test public void displayGreeting_timeIsMorning_useMorningSettings() { setTimeOfDay(TIME_MORNING); userGreeter.displayGreeting(); // Вызов SUT verify(userPrompt).setText(any(), eq("Good morning!"), any()); verify(userPrompt).setIcon(IMAGE_SUNSHINE); } 278 Глава 13. Тестирование с дублерами Заключение Мы узнали, что тестовые дублеры имеют решающее значение в ускорении разработки, потому что они помогают всесторонне тестировать код и выполнять тесты быстро. Но их неправильное использование может привести к значительному уменьшению продуктивности инженеров из-за ухудшения ясности, увеличения хрупкости и сни- жения эффективности тестов. Вот почему для инженеров важно иметь представление об эффективных практиках применения тестовых дублеров. Часто невозможно точно сказать, когда лучше использовать реальную реализацию, а когда тестовый дублер или какую методику тестирования с дублерами использо- вать. Инженер должен оценить некоторые компромиссы при выборе правильного подхода в конкретном случае. Тестовые дублеры отлично подходят для обхода зависимостей, которые трудно ис- пользовать в тестах, но если вы хотите максимально повысить доверие к тестовому коду, задействуйте эти зависимости в тестах. Следующая глава рассматривает круп- номасштабное тестирование, в котором зависимости используются, даже если они делают тесты медленными или недетерминированными. Итоги y В тестах предпочтительнее использовать реальную реализацию. y Если реальную реализацию невозможно использовать в тестах, примените ее имитацию. y Злоупотребление заглушками делает тесты неясными и хрупкими. y Желательно избегать тестирования взаимодействий: этот подход к тестированию делает тесты хрупкими, потому что в них проникают детали реализации SUT. ГЛАВА 14 Крупномасштабное тестирование Автор: Джозеф Грейвс Редактор: Том Маншрек В предыдущих главах мы рассказали, как в Google была создана культура тести- рования и как маленькие юнит-тесты стали фундаментальной частью рабочего процесса инженера. Но как обстоят дела с другими видами тестирования? В Google используется большое количество более масштабных тестов, которые стали частью политики снижения рисков в программной инженерии. Но эти тесты создают до- полнительные проблемы, и часто довольно сложно сделать их ценным активом, а не пожирателями ресурсов. В этой главе мы обсудим, что мы подразумеваем под «более масштабными тестами», когда мы их выполняем, и какие практики используем для повышения их эффективности. Что такое большие тесты? Как упоминалось выше, в Google используются свои определенные понятия о раз- мере теста. Маленькие тесты ограничиваются одним потоком выполнения или одним процессом, средние — одной машиной. У больших тестов нет таких ограничений. Но в Google также есть понятие широты охвата теста. Юнит-тест всегда имеет более узкую область охвата, чем интеграционный. А тесты с самой большой областью ох- вата (иногда называемые сквозными, или системными, тестами) обычно вовлекают в работу несколько реальных зависимостей и почти не используют тестовые дублеры. Большие тесты в отличие от других видов тестов: y могут работать медленно; время ожидания для наших больших тестов по умол- чанию составляет от 15 минут до 1 часа, но у нас также есть тесты, которые вы- полняются несколько часов или даже дней; y могут быть негерметичными; большие тесты могут использовать ресурсы со- вместно с другими тестами и обмениваться трафиком; y могут быть недетерминированными; если большой тест негерметичен, то почти невозможно гарантировать его детерминированное поведение из-за влияния других тестов или данных, вводимых пользователем. Так зачем нужны большие тесты? Подумайте о процессе программирования. Как убедиться, что программы действительно работают? Возможно, в процессе разработ- 280 Глава 14. Крупномасштабное тестирование ки вы пишете и запускаете юнит-тесты, но запускаете ли вы настоящий двоичный файл и пробуете ли его применить? А когда вы передаете свой код другим, как они проверяют его? Запускают свои юнит-тесты или пробуют его применить? Кроме того, как узнать, что ваш код продолжает работать после внесения измене- ний? Предположим, вы создаете сайт, который использует Google Maps API. Чтобы выявить проблемы его совместимости с новой версией API, вы не станете создавать юнит-тесты, а запустите этот сайт и попробуете его применить к новой версии API, чтобы увидеть, не появились ли какие-нибудь нарушения в его работе. Юнит-тесты могут дать уверенность в правильном поведении отдельных функций, объектов и модулей, а большие тесты дают уверенность в том, что вся система работает так, как задумано. Кроме того, автоматизированные тесты поддерживают возмож- ность масштабирования, которая отсутствует в ручном тестировании. |