У меня есть базовое понимание ложных и фиктивных объектов, но я не уверен, что у меня есть представление о том, когда и где использовать насмешки - особенно в том случае, если это применимо к этому сценарию здесь .
У меня есть базовое понимание ложных и фиктивных объектов, но я не уверен, что у меня есть представление о том, когда и где использовать насмешки - особенно в том случае, если это применимо к этому сценарию здесь .
Ответы:
Модульный тест должен проверять один путь кода с помощью одного метода. Когда выполнение метода передается за пределы этого метода, в другой объект и обратно, у вас появляется зависимость.
Когда вы тестируете этот путь кода с фактической зависимостью, вы не тестируете модуль; Вы тестирование интеграции. Хотя это хорошо и необходимо, это не модульное тестирование.
Если в вашей зависимости есть ошибки, это может повлиять на ваш тест и вернуть ложное срабатывание. Например, вы можете передать зависимости неожиданное значение 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
что ничего не делает. Таким образом, вы можете изменить его один раз для всех тестов, и ваши тесты не будут загромождены длинными настройками, в которых вы тренируете свои заглушки.В общем, фиктивные объекты имеют свое применение, но, когда они не используются осторожно, они часто поощряют плохие практики, тестируют детали реализации, затрудняют рефакторинг и производят трудные для чтения и сложные в обслуживании тесты .
Дополнительные сведения о недостатках макетов см. Также в разделе « Объекты-макеты: недостатки и варианты использования» .
Практическое правило:
Если для функции, которую вы тестируете, требуется сложный объект в качестве параметра, и было бы сложно просто создать экземпляр этого объекта (если, например, он пытается установить TCP-соединение), используйте макет.
Вы должны смоделировать объект, когда у вас есть зависимость в блоке кода, который вы пытаетесь проверить, который должен быть «просто так».
Например, когда вы пытаетесь протестировать некоторую логику в своей единице кода, но вам нужно получить что-то от другого объекта, и то, что возвращается из этой зависимости, может повлиять на то, что вы пытаетесь протестировать - имитируйте этот объект.
Отличный подкаст на эту тему можно найти здесь