Поддельные объекты полезны, когда вы хотите проверить взаимодействия между тестируемым классом и конкретным интерфейсом.
Например, мы хотим проверить, что метод 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
, который ничего не делает. Таким образом, вы можете изменить его один раз для всех тестов, и ваши тесты не будут перегружены длительными настройками, где вы тренируете свои заглушки.
В общем, фиктивные объекты имеют свое применение, но когда они не используются осторожно, они часто поощряют плохие практики, тестируют детали реализации, мешают рефакторингу и производят трудные для чтения и сложные в обслуживании тесты .
Подробнее о недостатках макетов см. Также Объекты-макеты: недостатки и варианты использования .