Делай как вGoogle
Скачать 5.77 Mb.
|
Как определить, когда лучше использовать реальную реализацию Реальная реализация предпочтительнее, если она работает быстро и детерминиро- ванно и имеет простые зависимости. Например, для объектов-значений всегда лучше использовать реальную реализацию ( https://oreil.ly/UZiXP ). К ним относятся классы, представляющие денежные суммы, даты, географические адреса или коллекции, такие как список или ассоциативный массив. Однако использование в тесте реальных реализаций с более сложным кодом часто неосуществимо. Не всегда можно точно сказать, что лучше использовать — реальную реализацию или тестовый дублер, поэтому, не забывая о существующих компромис- сах, вы должны руководствоваться следующими соображениями. Время выполнения Одним из наиболее важных качеств юнит-тестов является скорость: они должны постоянно запускаться во время разработки и быстро сообщать о работоспособности кода (а также быстро выполняться при запуске в системе непрерывной интеграции). В них тестовые дублеры могут быть полезны, если реальная реализация выполняется медленно. Но что значит «медленно» с точки зрения юнит-тестов? Если реальная реализация добавляет одну миллисекунду ко времени выполнения каждого отдельного теста, мало кто назовет ее медленной. А если она добавляет 10 миллисекунд, 100 милли- секунд, 1 секунду или больше? На этот вопрос нет точного ответа. Ответ зависит от того, насколько сильно инженеры ощущают потерю продуктивности во время выполнения реализации и сколько тестов используют реализацию (дополнительная секунда на тест выглядит приемлемой, когда имеется пять тестов, но не 500). Можно сначала использовать в юнит-тесте реальную реализацию, и если она станет слишком медленной, обновить тесты и ис- пользовать в них тестовый дублер. 266 Глава 13. Тестирование с дублерами Распараллеливание тестов тоже может помочь уменьшить общее время их выполне- ния. Наша тестовая инфраструктура в Google упрощает деление тестов на наборы, которые будут выполняться на нескольких серверах. Этот подход увеличивает за- траты процессорного времени, но экономит время разработчика. Подробнее об этом подходе в главе 18. Также помните еще об одном компромиссе: использование реальной реализации может увеличить время тестирования, если тестам придется выполнять сборку ре- альной реализации и всех ее зависимостей. Хорошо масштабируемая система сборки, такая как Bazel ( https://bazel.build ), может ослабить эту проблему за счет кеширования неизменяемых артефактов. Детерминированность Тест считается детерминированным ( https://oreil.ly/brxJl ), если для данной версии SUT выполнение теста всегда приводит к одному и тому же результату: тест либо всегда вы- полняется успешно, либо всегда терпит неудачу. Напротив, тест считается недетерми- нированным ( https://oreil.ly/5pG0f ), если его результат меняется при неизменности SUT. Отсутствие детерминированности в тестах ( https://oreil.ly/71OFU ) может порождать их нестабильное поведение, при котором тесты могут завершаться неудачей, даже если SUT не изменялась. Нестабильность вредит набору тестов, потому что разработчики начинают терять доверие к результатам тестирования и игнорировать сбои (глава 11). Если использование реальной реализации редко приводит к появлению случайных ошибок, эти ошибки не помешают работе инженеров. Но если нестабильное пове- дение тестов начинает проявляться слишком часто, значит, пришло время заменить реальную реализацию тестовым дублером, чтобы повысить достоверность теста. Реальная реализация может быть сложнее тестового дублера и больше него способна вызвать недетерминированное поведение в тесте. Например, реальная реализация, использующая многопоточность, иногда может вызывать сбой теста, если выходные данные SUT зависят от порядка выполнения потоков. Распространенной причиной отсутствия детерминированности в тесте является не- герметичный код ( https://oreil.ly/aes__ ), то есть код, зависящий от внешних служб, не- подконтрольных тесту. Например, тест, пытающийся получить веб-страницу с HTTP- сервера, может завершиться неудачей, если сервер перегружен или изменилось содержимое веб-страницы. Чтобы тест не зависел от внешнего сервера, используйте тестовые дублеры или герметичный экземпляр сервера, жизненный цикл которого управляется тестом. Подробнее о герметичных экземплярах в следующей главе. Еще недетерминированным считается тестовый код, опирающийся на системные часы. Если выходные данные SUT зависят от текущего времени, используйте тесто- вые дублеры, которые жестко кодируют определенное время. Создание зависимостей При использовании реальной реализации необходимо создать все ее зависимости. Например, для тестирования объекта нужно построить полное дерево его зависимо- Имитации 267 стей, в которое войдут все объекты, от которых он зависит, все объекты, от которых зависят эти объекты-зависимости, и т. д. Тестовый дублер часто не имеет зависимо- стей, поэтому создать его проще, чем реальную реализацию. Представьте, что вы создаете объекты внутри теста. Может потребоваться немало времени, чтобы выяснить, как создать каждый отдельный объект. Кроме того, тесты требуют постоянного обслуживания, потому что они должны обновляться при из- менении сигнатур конструкторов этих объектов: Foo foo = new Foo(new A(new B(new C()), new D()), new E(), ..., new Z()); В таких случаях может показаться заманчивым использование тестового дублера. Например, вот все, что нужно для создания тестового дублера с помощью фрейм- ворка Mockito: @Mock Foo mockFoo; Да, создать тестовый дублер просто, но использование реальной реализации дает значительные преимущества, которые мы обсудили выше в этом разделе. Кроме того, у чрезмерного использования тестовых дублеров есть существенные недостатки, которые мы рассмотрим далее в этой главе. Таким образом, только компромиссы помогают нам сделать выбор между реальной реализацией и тестовым дублером. Вместо создания вручную объектов в тестах идеальным решением было бы при- менение того же кода, который используется в самой системе, например с по- мощью фабричного метода или автоматического внедрения зависимостей. Для поддержки тестов такой код должен быть достаточно гибким, чтобы можно было использовать тестовые дублеры вместо жестко заданных реализаций, использу- емых в продакшене. Имитации В случае невозможности использовать реальную реализацию в тесте лучшим вари- антом часто оказывается использование имитаций. Имитация предпочтительнее других видов тестовых дублеров, потому что она действует подобно реальной реа- лизации: SUT может даже не определить, с чем она взаимодействует — с реальной реализацией или с имитацией. В листинге 13.11 показана имитация файловой системы. Листинг 13.11. Имитация файловой системы // Эта имитация реализует интерфейс FileSystem. Этот же интерфейс // используется реальной реализацией public class FakeFileSystem implements FileSystem { // Ассоциативный массив, отображающий имена файлов в их содержимое. // Файлы хранятся в памяти, а не на диске, потому что в тестах // нежелательно выполнять операции ввода/вывода с диском private Map 268 Глава 13. Тестирование с дублерами @Override public void writeFile(String fileName, String contents) { // Добавить имя файла и его содержимое в ассоциативный массив files.add(fileName, contents); } @Override public String readFile(String fileName) { String contents = files.get(fileName); // Реальная реализация сгенерирует это исключение, если файл // не будет найден, поэтому имитация тоже генерирует его if (contents == null) { throw new FileNotFoundException(fileName); } return contents; } } Почему имитации так важны? Имитации могут быть мощным инструментом тестирования: они быстро выпол- няются и позволяют эффективно тестировать код, избавляя его от недостатков ис- пользования реальных реализаций. Единственная имитация может радикально улучшить качество тестирования API. А если реализовать большое количество имитаций для всех видов API, то они могут значительно повысить скорость разработки. В софтверной компании, в которой имитации используются редко, скорость разра- ботки будет низкой, потому что инженеры используют в тестах реальные реализа- ции, из-за которых тесты получаются медленными и нестабильными, или тестовые дублеры (тестирование взаимодействий, заглушки), которые, как будет показано далее в этой главе, могут привести к созданию неясных и хрупких тестов. Когда следует писать имитации? Для создания имитации требуется много усилий и опыта. Имитация тоже требует обслуживания: всякий раз, когда меняется поведение реальной реализации, необхо- димо соответствующим образом обновить ее имитацию. По этой причине команда, которой принадлежит реальная реализация, должна сама написать и поддерживать ее имитацию. Рассматривая вопрос о создании имитации, команда должна определить, перевесит ли увеличение продуктивности, обусловленное использованием имитации, затраты на ее написание и обслуживание. Если имитация будет использоваться лишь в паре тестов, тогда она не будет стоить времени, потраченного на ее создание, но если таких тестов сотни, разработка имитации даст существенный прирост продуктивности. Чтобы уменьшить количество имитаций, которые необходимо поддерживать, инженеры создают их только для самого основного кода, который невозможно ис- Имитации 269 пользовать в тестах. Например, если базу данных нельзя использовать в тестах, то следует создать имитацию самого API базы данных, а не каждого класса, который этот API вызывает. Поддержка имитации может оказаться обременительным делом, если ее реализацию необходимо повторить на разных языках программирования. Представьте имитацию службы, имеющей клиентские библиотеки для обращения к ней из разных языков. Одно из решений для ее поддержки — создать единую реализацию имитируемой службы и использовать в тестах клиентские библиотеки для отправки запросов. Это более сложный подход по сравнению с размещением имитации в памяти, потому что он требует тестировать взаимодействия между процессами. Однако он может стать разумным компромиссом, если тесты выполняются быстро. Достоверность имитаций Пожалуй, самой важной характеристикой имитации является ее достоверность — мера соответствия поведения реальной реализации поведению ее имитации. Если поведение имитации не соответствует поведению реальной реализации, тест с такой имитацией бесполезен — один и тот же тестовый код может выполниться успешно с имитаций и завершиться неудачей с реальной реализацией. Идеальная достоверность возможна далеко не всегда. В конце концов, имитация ис- пользуется в тестах, потому что реальная реализация не подходит для тестирования по каким-то причинам. Например, имитация базы данных обычно недостоверно имитирует реальную базу данных с точки зрения хранения данных на жестком дис- ке, потому что будет хранить все данные в памяти. Однако в первую очередь имитация должна поддерживать контракты API реальной реализации. Для любых входных данных имитация должна возвращать тот же резуль- тат и так же изменять свое состояние, как и реальная реализация. Например, если реальная реализация database.save(itemId) успешно сохраняет элемент, указанного идентификатора которого пока не существует, и генерирует ошибку в противном случае, то имитация должна действовать так же. То есть имитация должна иметь идеальную достоверность только с точки зрения теста. Например, имитация API хеширования не должна гарантировать точное со- впадение значения хеша для данного ввода со значением, сгенерированным реальной реализацией. В тестах, скорее всего, будет проверяться не конкретное значение хеша, а его уникальность для данного ввода. Если контракт API-хеширования не гаранти- рует возврат конкретных значений, то имитация будет соответствовать контракту, даже если не обладает идеальной достоверностью. В числе других примеров, когда идеальная достоверность не имеет большого значения для имитаций, можно назвать задержки и потребление ресурсов. Однако имитация не может использоваться для явной проверки этих показателей (например, в тесте производительности, проверяющем задержку вызова функции) — в таких случаях лучше использовать реальную реализацию. 270 Глава 13. Тестирование с дублерами Имитация не должна обладать всеми функциональными возможностями соответ- ствующей реальной реализации, особенно если этого не требует большинство тестов. Например, в таких случаях, как обработка редких ошибок, лучше, чтобы имитация быстро вышла из строя. Для этого можно вызывать сбой теста при попытке обра- титься к возможностям, не реализованным в имитации. Полученная ошибка сообщит инженеру, что имитация не подходит для данной ситуации. Имитации должны тестироваться Имитация должна иметь собственные тесты, чтобы инженер мог убедиться, что она соответствует API реальной реализации. Имитация без тестов может первоначально показывать реалистичное поведение, но по мере развития реальной реализации по- ведение реальной реализации и имитации может начать отличаться. Один из подходов к разработке тестов для имитаций предполагает написание тестов для общедоступного API и проверку с их помощью реальной реализации и имита- ции (такие тесты еще называют контрактными ( https://oreil.ly/yuVlX )). Тесты, ис- пользующие реальную реализацию, скорее всего, будут выполняться медленно, но этот их недостаток не имеет существенного значения, потому что эти тесты должны запускаться только владельцами имитации. Что делать в отсутствие имитации Если требуемая имитация недоступна, обратитесь к владельцам API с предложением создать ее. Владельцы могут быть не знакомы с идеей имитаций или не осознавать преимуществ, которые имитации дают пользователям API. Если владельцы API не хотят или не могут создать имитацию, попробуйте написать ее сами. Для этого можно заключить все вызовы API в один класс, а затем создать имитационную версию класса, которая не взаимодействует с API. Это может оказать- ся намного проще, чем создавать имитацию для всего API, потому что на практике часто используется только подмножество API. Некоторые команды в Google даже делятся своими имитациями с владельцами API, что позволяет другим командам тоже использовать эти имитации. Наконец, можно просто использовать реальную реализацию (с ее достоинствами и недостатками, о которых мы говорили выше в этой главе) или прибегнуть к дру- гим методам тестирования с дублерами (о достоинствах и недостатках которых мы расскажем далее в этой главе). В некоторых случаях имитацию можно рассматривать как оптимизацию: если тесты, использующие реальные реализации, выполняются слишком медленно, можно со- здать имитацию, чтобы заставить их работать быстрее. Но если выгоды от ускорения тестирования не перевешивают затрат на создание и поддержку имитации, то лучше оставить в тесте реальную реализацию. Заглушки 271 Заглушки Как обсуждалось выше в этой главе, заглушки помогают запрограммировать в тесте определенное поведение функции, которое иначе недоступно. Часто это простой и быстрый способ заменить реальную реализацию в тесте. Например, код в лис- тинге 13.12 использует заглушку для эмуляции ответа сервера, обслуживающего кредитные карты. Листинг 13.12. Использование заглушки для эмуляции ответа сервера @Test public void getTransactionCount() { transactionCounter = new TransactionCounter(mockCreditCardServer); // Использовать заглушку для возврата трех транзакций when(mockCreditCardServer.getTransactions()).thenReturn( newList(TRANSACTION_1, TRANSACTION_2, TRANSACTION_3)); assertThat(transactionCounter.getTransactionCount()).isEqualTo(3); } Опасности злоупотребления заглушками Заглушки очень просты в использовании, поэтому у вас может возникнуть соблазн использовать их взамен любой нетривиальной реальной реализации. Однако зло- употребление заглушками в тестах может привести к значительным потерям про- дуктивности инженеров, которые должны будут поддерживать эти тесты. Тесты становятся менее ясными Чтобы создать заглушку, нужно написать дополнительный код, определяющий по- ведение заглушаемых функций. Этот дополнительный код отвлекает от главной цели теста, и его трудно понять, если вы не знакомы с реализацией SUT. Если вы ловите себя на мысли, что пытаетесь вникнуть в особенности SUT, чтобы понять, почему некоторые функции в тесте заглушены, это верный признак того, что заглушка не подходит для этого теста. Тесты становятся хрупкими Заглушки способствуют проникновению деталей реализации вашего кода в тест. Когда детали реализации в коде изменятся, вам придется обновить тесты, чтобы отразить эти изменения. В идеале хороший тест должен изменяться только при из- менении поведения API, обращенного к пользователю, и на тест не должны влиять изменения в реализации API. Тесты становятся менее эффективными При использовании заглушек нет гарантий, что они будут действовать подобно реальной реализации. Например, следующая инструкция жестко кодирует часть контракта метода add() («Если передать методу числа 1 и 2, он вернет число 3»): when(stubCalculator.add(1, 2)).thenReturn(3); 272 Глава 13. Тестирование с дублерами Заглушка — плохой выбор, если SUT зависит от контракта реальной реализации, потому что вынуждает дублировать в тесте детали контракта и не гарантирует его правильность (то есть достоверность заглушки). Кроме того, заглушки не позволяют сохранить состояние, что может затруднить тестирование некоторых аспектов кода. Например, после вызова метода database. save(item) реальной реализации или имитации можно получить объект item обратно, вызвав database.get(item.id()) , если, конечно, оба метода обращаются к внутрен- нему состоянию, но сделать то же самое с заглушкой нет никакой возможности. |