У меня есть базовое представление о фиктивных и фальшивых объектах, но я не уверен, что у меня есть ощущение, когда и где использовать имитацию, особенно в том смысле, что это применимо к этому сценарию здесь.
Когда я должен издеваться?
Ответы (4)
Модульный тест должен проверять один кодовый путь одним методом. Когда выполнение метода переходит за пределы этого метода в другой объект и обратно, у вас возникает зависимость.
Когда вы тестируете этот путь кода с фактической зависимостью, вы не выполняете модульное тестирование; вы проводите интеграционное тестирование. Хотя это хорошо и необходимо, это не модульное тестирование.
Если ваша зависимость содержит ошибки, это может повлиять на ваш тест и вернуть ложное срабатывание. Например, вы можете передать зависимости неожиданное значение NULL, и зависимость может не передавать значение NULL, как это описано в документации. Ваш тест не обнаруживает исключения с нулевым аргументом, как должно быть, и тест проходит.
Кроме того, вам может быть сложно, если вообще возможно, надежно заставить зависимый объект возвращать именно то, что вы хотите во время теста. Это также включает в себя выдачу ожидаемых исключений в тестах.
Мок заменяет эту зависимость. Вы устанавливаете ожидания для вызовов зависимого объекта, устанавливаете точные возвращаемые значения, которые он должен дать вам для выполнения желаемого теста, и / или какие исключения генерировать, чтобы вы могли протестировать свой код обработки исключений. Таким образом, вы можете легко протестировать рассматриваемое устройство.
TL; DR: имитируйте каждую зависимость, которой касается ваш модульный тест.
Мок-объекты полезны, когда вы хотите протестировать взаимодействия между тестируемым классом и определенным интерфейсом.
Например, мы хотим протестировать этот метод sendInvitations(MailServer mailServer)
вызывает MailServer.createMessage()
ровно один раз, а также вызывает MailServer.sendMessage(m)
ровно один раз, и никакие другие методы не вызываются в интерфейсе MailServer
. Это когда мы можем использовать фиктивные объекты.
С помощью фиктивных объектов вместо передачи реального MailServerImpl
или теста TestMailServer
мы можем передать фиктивную реализацию интерфейса MailServer
. Перед тем, как передать фиктивный MailServer
, мы обучаем его, чтобы он знал, какие вызовы методов следует ожидать и какие возвращаемые значения возвращать. В конце фиктивный объект утверждает, что все ожидаемые методы были вызваны должным образом.
В теории это звучит неплохо, но есть и недостатки.
Имитация недостатков
Если у вас есть фиктивный фреймворк, у вас возникает соблазн использовать фиктивный объект каждый раз, когда вам нужно передать интерфейс тестируемому классу. Таким образом вы завершите тестирование взаимодействия, даже если в этом нет необходимости. К сожалению, нежелательное (случайное) тестирование взаимодействий - это плохо, потому что тогда вы проверяете, что конкретное требование реализовано определенным образом, а не то, что реализация дала требуемый результат.
Вот пример в псевдокоде. Предположим, мы создали класс MySorter
и хотим его протестировать:
// the correct way of testing
testSort() {
testList = [1, 7, 3, 8, 2]
MySorter.sort(testList)
assert testList equals [1, 2, 3, 7, 8]
}
// incorrect, testing implementation
testSort() {
testList = [1, 7, 3, 8, 2]
MySorter.sort(testList)
assert that compare(1, 2) was called once
assert that compare(1, 3) was not called
assert that compare(2, 3) was called once
....
}
(В этом примере мы предполагаем, что это не конкретный алгоритм сортировки, такой как быстрая сортировка, который мы хотим протестировать; в этом случае последний тест действительно будет действительным.)
В таком крайнем примере очевидно, почему последний пример неверен. Когда мы меняем реализацию MySorter
, первый тест отлично справляется с задачей убедиться, что мы по-прежнему правильно сортируем, и в этом весь смысл тестов - они позволяют нам безопасно изменять код. С другой стороны, последний тест всегда дает сбой и активно вреден; это мешает рефакторингу.
Моки как заглушки
Мок-фреймворки часто допускают также менее строгое использование, когда нам не нужно точно указывать, сколько раз должны вызываться методы и какие параметры ожидаются; они позволяют создавать фиктивные объекты, которые используются как заглушки.
Предположим, у нас есть метод sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer)
, который мы хотим протестировать. Объект PdfFormatter
можно использовать для создания приглашения. Вот тест:
testInvitations() {
// train as stub
pdfFormatter = create mock of PdfFormatter
let pdfFormatter.getCanvasWidth() returns 100
let pdfFormatter.getCanvasHeight() returns 300
let pdfFormatter.addText(x, y, text) returns true
let pdfFormatter.drawLine(line) does nothing
// train as mock
mailServer = create mock of MailServer
expect mailServer.sendMail() called exactly once
// do the test
sendInvitations(pdfFormatter, mailServer)
assert that all pdfFormatter expectations are met
assert that all mailServer expectations are met
}
В этом примере нам не очень важен объект PdfFormatter
, поэтому мы просто обучаем его спокойно принимать любой вызов и возвращать некоторые разумные стандартные возвращаемые значения для всех методов, которые sendInvitation()
вызывают в этот момент. Как мы пришли к именно этому списку методов тренировки? Мы просто запустили тест и продолжали добавлять методы, пока тест не прошел. Обратите внимание, что мы обучили заглушку реагировать на метод, не понимая, почему он должен его вызывать, мы просто добавили все, на что жаловался тест. Довольны, тест пройден.
Но что произойдет позже, когда мы изменим sendInvitations()
или какой-нибудь другой класс, который sendInvitations()
использует, для создания более причудливых PDF-файлов? Наш тест внезапно терпит неудачу, потому что теперь вызывается больше методов PdfFormatter
, и мы не научили нашу заглушку их ожидать. И обычно в таких ситуациях терпит неудачу не только один тест, но и любой тест, в котором прямо или косвенно используется метод sendInvitations()
. Мы должны исправить все эти тесты, добавив больше тренингов. Также обратите внимание, что мы не можем удалить методы, которые больше не нужны, потому что мы не знаем, какие из них не нужны. Опять же, это мешает рефакторингу.
Кроме того, ужасно страдала удобочитаемость теста, там много кода, который мы писали не потому, что хотели, а потому, что должны были; это не мы хотим, чтобы этот код был там. Тесты, в которых используются фиктивные объекты, выглядят очень сложными и часто трудночитаемыми. Тесты должны помочь читателю понять, как следует использовать тестируемый класс, поэтому они должны быть простыми и понятными. Если они не читаются, никто не будет их поддерживать; на самом деле их легче удалить, чем поддерживать.
Как это исправить? С легкостью:
- По возможности старайтесь использовать настоящие классы вместо моков. Используйте настоящий
PdfFormatterImpl
. Если это невозможно, измените реальные классы, чтобы это стало возможным. Невозможность использовать класс в тестах обычно указывает на некоторые проблемы с классом. Устранение проблем - беспроигрышная ситуация: вы исправили класс и у вас есть более простой тест. С другой стороны, не исправлять это и использовать моки - это безвыходная ситуация: вы не исправили реальный класс, и у вас есть более сложные, менее читаемые тесты, которые мешают дальнейшему рефакторингу. - Попробуйте создать простую тестовую реализацию интерфейса вместо того, чтобы имитировать его в каждом тесте, и используйте этот тестовый класс во всех своих тестах. Создать
TestPdfFormatter
, который ничего не делает. Таким образом, вы можете изменить его один раз для всех тестов, и ваши тесты не будут загромождены длинными настройками, на которых вы тренируете свои заглушки.
В общем, имитирующие объекты имеют свое применение, но при неосторожном использовании они часто поощряют плохие практики, тестируют детали реализации, препятствуют рефакторингу и создают трудные для чтения и трудные для сопровождения тесты.
Для получения дополнительной информации о недостатках имитаций см. Также Mock Objects: Insights and Примеры использования.
Практическое правило:
Если для тестируемой функции требуется сложный объект в качестве параметра, и было бы сложно просто создать экземпляр этого объекта (если, например, он пытается установить TCP-соединение), используйте макет.
Вы должны имитировать объект, когда у вас есть зависимость в блоке кода, который вы пытаетесь протестировать, который должен быть «именно таким».
Например, когда вы пытаетесь протестировать некоторую логику в своей единице кода, но вам нужно получить что-то от другого объекта, и то, что возвращается из этой зависимости, может повлиять на то, что вы пытаетесь протестировать - имитируйте этот объект.
Отличный подкаст по этой теме можно найти здесь