Делай как вGoogle
Скачать 5.77 Mb.
|
Листинг 12.19. Тесты, чрезмерно близко следующие принципу DRY @Test public void shouldAllowMultipleUsers() { List Forum forum = createForumAndRegisterUsers(users); validateForumAndUsers(forum, users); } @Test public void shouldNotAllowBannedUsers() { List Forum forum = createForumAndRegisterUsers(users); validateForumAndUsers(forum, users); } // Множество других тестов... private static List List for (boolean isBanned : banned) { users.add(newUser() .setState(isBanned ? State.BANNED : State.NORMAL) .build()); } return users; } private static Forum createForumAndRegisterUsers(List Forum forum = new Forum(); for (User user : users) { try { forum.register(user); } catch(BannedUserException ignored) {} } return forum; } private static void validateForumAndUsers(Forum forum, List assertThat(forum.isReachable()).isTrue(); Повторное использование тестов и кода: DAMP, не DRY 249 for (User user : users) { assertThat(forum.hasRegisteredUser(user)) .isEqualTo(user.getState() == State.BANNED); } } Проблемы в этом коде очевидны. Во-первых, тестовые методы краткие, но не полные: важные детали скрыты за вызовами вспомогательных методов, которые читатель не может увидеть, не прокрутив содержимое редактора. Кроме того, эти вспомогатель- ные методы содержат логику, которую трудно понять с первого взгляда (вы смогли заметить ошибку?). Тест станет намного понятнее, если переписать его, следуя принципу DAMP: Листинг 12.20. Тесты должны следовать принципу DAMP @Test public void shouldAllowMultipleUsers() { User user1 = newUser().setState(State.NORMAL).build(); User user2 = newUser().setState(State.NORMAL).build(); Forum forum = new Forum(); forum.register(user1); forum.register(user2); assertThat(forum.hasRegisteredUser(user1)).isTrue(); assertThat(forum.hasRegisteredUser(user2)).isTrue(); } @Test public void shouldNotRegisterBannedUsers() { User user = newUser().setState(State.BANNED).build(); Forum forum = new Forum(); try { forum.register(user); } catch(BannedUserException ignored) {} assertThat(forum.hasRegisteredUser(user)).isFalse(); } В этих тестах больше повторяющегося кода и методы длиннее, но смысл каждого отдельного теста объяснен в пределах тела метода. При чтении этих тестов инженер может быть уверен, что тесты делают именно то, что заявляют, и не скрывают ошибок. Принцип DAMP служит не заменой, а дополнением к принципу DRY. Он не запре- щает использовать вспомогательные методы и исключать повторяющиеся шаги, если эти меры улучшают описательность и осмысленность тестов. В оставшейся части этого раздела мы рассмотрим общие паттерны совместного использования кода в нескольких тестах. 250 Глава 12. Юнит-тестирование Общие значения Часто наборы тестов определяют множество общих значений, используемых от- дельными тестами для проверки различных случаев применения этих значений. Листинг 12.21 иллюстрирует такие тесты. Листинг 12.21. Общие значения с неоднозначными именами private static final Account ACCOUNT_1 = Account.newBuilder() .setState(AccountState.OPEN).setBalance(50).build(); private static final Account ACCOUNT_2 = Account.newBuilder() .setState(AccountState.CLOSED).setBalance(0).build(); private static final Item ITEM = Item.newBuilder() .setName("Cheeseburger").setPrice(100).build(); // Сотни других тестов... @Test public void canBuyItem_returnsFalseForClosedAccounts() { assertThat(store.canBuyItem(ITEM, ACCOUNT_1)).isFalse(); } @Test public void canBuyItem_returnsFalseWhenBalanceInsufficient() { assertThat(store.canBuyItem(ITEM, ACCOUNT_2)).isFalse(); } Эта стратегия позволяет делать тесты очень краткими, но может стать источником проблем при росте набора тестов. Иногда бывает трудно понять, почему для теста выбрано определенное значение. К счастью, имена тестов в листинге 12.21 поясня- ют, какие сценарии тестируются, но читатель все равно вынужден прокрутить код, чтобы убедиться, что ACCOUNT_1 и ACCOUNT_2 подходят для этих сценариев. Для яс- ности можно использовать описательные имена констант (например, CLOSED_ACCOUNT и ACCOUNT_WITH_LOW_BALANCE ), но тогда потребуется дополнительно проверить детали тестируемого значения, а простота повторного использования констант может по- будить инженеров задействовать их, даже когда имена этих констант не совсем точно соответствуют тесту. Инженеры предпочитают определять и использовать общие константы, потому что объявлять отдельные значения в каждом тесте довольно утомительно. Лучший способ достижения этой цели — конструирование данных с использованием вспомогательных методов ( https://oreil.ly/Jc4VJ ) (листинг 12.22), которые требуют, чтобы автор теста указал только необходимые значения, и устанавливают разумные значения по умолчанию 1 1 Часто полезно также добавить немного случайности в выбор значений по умолчанию, не за- данных явно. Это поможет гарантировать, что два разных экземпляра не будут оцениваться Повторное использование тестов и кода: DAMP, не DRY 251 для всех остальных констант. Такое конструирование легко реализовать в языках, поддерживающих именованные параметры, а в других языках можно использовать, например, паттерн Строитель для их эмуляции (часто с помощью таких инструмен- тов, как AutoValue ( https://oreil.ly/cVYK6 )): Листинг 12.22. Определение общих значений с помощью вспомогательных методов # Вспомогательный метод обертывает конструктор, определяя произвольные # значения по умолчанию для всех его параметров def newContact( firstName="Grace", lastName="Hopper", phoneNumber="555-123-4567"): return Contact(firstName, lastName, phoneNumber) # Тесты вызывают вспомогательный метод и явно определяют только необходимые # параметры def test_fullNameShouldCombineFirstAndLastNames(self): def contact = newContact(firstName="Ada", lastName="Lovelace") self.assertEqual(contact.fullName(), "Ada Lovelace") // Некоторые языки, такие как Java, не поддерживающие именованные параметры, // могут эмулировать их, возвращая изменяемый объект "builder", который // представляет конструируемое значение private static Contact.Builder newContact() { return Contact.newBuilder() .setFirstName("Grace") .setLastName("Hopper") .setPhoneNumber("555-123-4567"); } // В этих языках тесты могут вызывать методы объекта builder, чтобы // переопределить только необходимые параметры, а затем вызвать // build(), чтобы создать фактический экземпляр builder @Test public void fullNameShouldCombineFirstAndLastNames() { Contact contact = newContact() .setFirstName("Ada") .setLastName("Lovelace") .build(); assertThat(contact.getFullName()).isEqualTo("Ada Lovelace"); } Использование вспомогательных методов для построения таких значений по- зволяет каждому тесту создавать значения, которые не приведут к конфликту между тестами. как равные, и не позволит инженерам жестко задавать в коде зависимости от этих значений по умолчанию. 252 Глава 12. Юнит-тестирование Общие настройки Еще один прием для совместного использования кода в тестах — определение общей логики настройки и инициализации. Многие фреймворки тестирования позволяют определять методы, которые будут выполняться перед каждым тестом в наборе. При правильном использовании эти методы могут сделать тесты более ясными и кратки- ми и избавить их от повторений кода и неуместной логики инициализации. Однако неправильное использование этих методов может повредить полноте теста и скрыть важные детали в отдельном методе инициализации. Наилучший вариант использования методов, выполняющих настройку, — создать тестируемый объект вместе с его зависимостями. Этот прием удобен, когда для большинства тестов в наборе не важны конкретные аргументы, применяемые для создания тестируемых объектов, и допускается использовать значения по умолча- нию. Эта же идея применима и к заглушкам возвращаемых значений в тестовых дублерах (глава 13). Один из недостатков методов настройки — они могут ухудшить ясность тестов, если эти тесты зависят от конкретных значений, используемых при настройке. Напри- мер, тест в листинге 12.23 выглядит неполным, потому что он вынуждает читателя выяснять, откуда взялась строка "Donald Knuth" Листинг 12.23. Зависимость от значений, определяемых в методах настройки private NameService nameService; private UserStore userStore; @Before public void setUp() { nameService = new NameService(); nameService.set("user1", "Donald Knuth"); userStore = new UserStore(nameService); } // [... сотни других тестов ...] @Test public void shouldReturnNameFromService() { UserDetails user = userStore.get("user1"); assertThat(user.getName()).isEqualTo("Donald Knuth"); } Тесты, которые явно зависят от конкретных значений, должны указывать эти значе- ния непосредственно, при необходимости переопределяя значения по умолчанию, создаваемые методом настройки. Конечно, это приведет к повторению кода, как по- казано в листинге 12.24, но в результате тест получается гораздо более описательным и осмысленным. Повторное использование тестов и кода: DAMP, не DRY 253 Листинг 12.24. Переопределение значений, создаваемых в методе настройки private NameService nameService; private UserStore userStore; @Before public void setUp() { nameService = new NameService(); nameService.set("user1", "Donald Knuth"); userStore = new UserStore(nameService); } @Test public void shouldReturnNameFromService() { nameService.set("user1", "Margaret Hamilton"); UserDetails user = userStore.get("user1"); assertThat(user.getName()).isEqualTo("Margaret Hamilton"); } Общие вспомогательные методы и проверки Последний распространенный способ совместного использования кода в тестах — «вспомогательные методы», которые вызываются из тестовых методов. Выше мы сказали, что некоторые вспомогательные методы полезны для конструирования общих тестовых значений, но другие виды вспомогательных методов могут быть опасными. Одной из распространенных разновидностей вспомогательных методов являются методы, выполняющие общий набор утверждений (проверок), относящихся к SUT. Например, метод validate , вызываемый в конце каждого тестового метода, выполня- ющего ряд фиксированных проверок, заставляет тесты обращать меньше внимания на тестирование поведения. Читая такие тесты, вы не сможете определить цель каж- дого из них. При появлении ошибки метод validate может затруднить локализацию тестов и распространит проблему на большое количество тестов в наборе. Однако более сфокусированные методы проверки все же могут быть полезны. Же- лательно, чтобы такие вспомогательные методы проверяли один конкретный факт о своих входных данных, а не целый ряд условий. Такие методы особенно полезны, когда для проверки концептуально простого условия требуется использовать циклы или условную логику, которая снизила бы ясность теста, если была бы включена в тело тестового метода. Например, вспомогательный метод в листинге 12.25 может приго- диться в тесте, охватывающем несколько разных случаев доступа к учетной записи. Листинг 12.25. Концептуально простой тест private void assertUserHasAccessToAccount(User user, Account account) { for (long userId : account.getUsersWithAccess()) { if (user.getId() == userId) { 254 Глава 12. Юнит-тестирование return; } } fail(user.getName() + " cannot access " + account.getName()); } Определение тестовой инфраструктуры Методы, которые мы обсудили выше, охватывают совместное использование кода в одном тестовом классе или наборе. Иногда бывает полезно определить общий код для использования в нескольких наборах тестов. Мы называем такой код тестовой инфраструктурой. Она подходит для интеграционного или сквозного тестирования и при хорошей организации может значительно облегчить написание юнит-тестов. Разработка тестовой инфраструктуры требует более тщательного подхода, чем раз- работка общего кода, который используется в единственном наборе тестов. Во многих отношениях код тестовой инфраструктуры больше похож на код продакшена, чем на тестовый, потому что он может вызываться из множества разных наборов и нередко его сложно изменить, не нарушив работоспособность тестов. В большинстве случаев предполагается, что инженеры не должны вносить изменения в общую тестовую инфраструктуру, чтобы протестировать свои функции. Тестовая инфраструктура должна рассматриваться как отдельный продукт и соответственно всегда должна иметь собственные тесты. Значительная часть тестовой инфраструктуры, которую использует большин- ство инженеров, представлена в виде известных сторонних библиотек, например JUnit ( https://junit.org ). Таких библиотек великое множество, и их стандартизация внутри организации должна проводиться как можно раньше и как можно шире. На- пример, много лет назад фреймворк Mockito был объявлен в Google единственной платформой, которая должна использоваться в новых тестах на Java, и было запре- щено использовать другие фреймворки в новых тестах. Тогда этот запрет вызвал не- довольство у сотрудников, знакомых с другими фреймворками, но в настоящее время он воспринимается как верный шаг, облегчивший понимание и работу наших тестов. Заключение Юнит-тесты — один из самых мощных инструментов, которые помогают инжене- рам-программистам убедиться, что системы сохранят работоспособность даже после непредвиденных изменений. Но широкие возможности подразумевают большую ответственность, и в результате неосторожного использования юнит-тестирования можно получить систему, которую будет трудно обслуживать и изменять. Юнит-тестирование в Google еще далеко от совершенства, но мы поняли, что сле- дование методикам, изложенным в этой главе, на несколько порядков увеличивает ценность тестов. Мы надеемся, что вы будете применять эти методики в своей работе! Итоги 255 Итоги y Стремитесь к неизменности тестов. y Тестируйте код через общедоступные API. y Тестируйте состояние, а не взаимодействия. y Пишите полные и краткие тесты. y Тестируйте поведение, а не методы. y Структурируйте тесты так, чтобы они подчеркивали поведение. y Подбирайте для тестов имена, отражающие тестируемое поведение. y Не вставляйте логику в тесты. y Пишите ясные сообщения об ошибках. y Используйте принцип DAMP вместо DRY при использовании общего кода в тестах. ГЛАВА 13 Тестирование с дублерами Авторы: Эндрю Тренк и Диллон Блай Редактор: Том Маншрек Юнит-тесты поддерживают продуктивность разработчиков и уменьшают количество дефектов в коде. Их легко писать для простого кода, но с увеличением сложности кода растет и сложность написания тестов. Представьте тестирование функции, которая отправляет запрос внешнему серверу и сохраняет ответ в базе данных. Выполнить несколько тестов такой функции можно, потратив не так много времени. Но на выполнение сотни или тысячи подобных тестов может уйти несколько часов, а наборы таких тестов могут стать нестабильным из-за случайных сбоев в сети или взаимовлияния тестов друг с другом. В таких случаях применяются тестовые дублеры ( https://oreil.ly/vbpiU ) — объекты или функции, способные заменить в тесте действительную реализацию, подобно тому как каскадер-дублер может заменить актера на съемочной площадке. Использование тестовых дублеров часто называют имитированием, но мы постараемся избегать этого термина в этой главе, потому что, как будет показано далее, этот термин также исполь- зуется для обозначения других аспектов, имеющих отношение к тестовым дублерам. Самой очевидной разновидностью тестового дублера является упрощенная реали- зация, которая действует подобно реальной, например как база данных в памяти. Другие виды тестовых дублеров позволяют проверить конкретные детали системы, например упрощают вызов редкой ошибки или гарантируют, что вызов тяжеловесной функции будет происходить без фактического выполнения реализации этой функции. В двух предыдущих главах мы познакомились с понятием маленьких тестов и об- судили, почему они должны составлять основу набора тестов. Однако код содержит взаимодействия между несколькими процессами или машинами, тестировать которые легче с помощью тестовых дублеров, позволяющих написать множество маленьких тестов, быстрых и стабильных в выполнении. Влияние тестовых дублеров на разработку ПО Использование тестовых дублеров вносит некоторые сложности в разработку ПО и требует определенных компромиссов. В этой главе мы подробно обсудим следу- ющие понятия: Тестовые дублеры в Google 257 Тестируемость Чтобы использовать тестовые дублеры, база кода должна быть спроектирована с учетом возможности ее тестирования — тесты должны иметь возможность заменять реальные реализации тестовыми дублерами. Например, код, обраща- ющийся к базе данных, должен быть достаточно гибким, чтобы тест для этого кода имел возможность использовать тестового дублера вместо реальной базы данных. Если база кода изначально не поддерживает возможность тестирования, вам потребуется приложить массу усилий, чтобы реорганизовать код для под- держки тестовых дублеров. Применимость Правильное применение тестовых дублеров может значительно увеличить скорость разработки, а при неправильном их использовании (особенно в большой кодовой базе) вы получите хрупкие, сложные и неэффективные тесты, которые будут снижать продуктивность инженеров. Если тестовые дублеры не могут использо- ваться в конкретном случае, инженер должен применить реальные реализации. Достоверность Под достоверностью понимается близость поведения тестового дублера к по- ведению реальной реализации, которую он заменяет. Если в этих поведениях есть существенные отличия, тесты, использующие такого дублера, скорее всего, не принесут большой пользы. Представьте, насколько полезен тестовый дублер, который имитирует базу данных, игнорирует любые попытки записи данных в него и всегда возвращает пустые результаты. Но идеальная достоверность может оказаться невозможной: тестовые дублеры часто должны быть намного проще реальных реализаций. Во многих ситуациях допустимо использовать дублеров даже без идеальной достоверности. Юнит-тесты, использующие дублеров, часто должны дополняться более масштабными тестами, исследующими реальную реализацию. |