Когда я должен издеваться?

138

У меня есть базовое понимание ложных и фиктивных объектов, но я не уверен, что у меня есть представление о том, когда и где использовать насмешки - особенно в том случае, если это применимо к этому сценарию здесь .

Эстебан Арайя
источник
Я рекомендую только имитировать внепроцессные зависимости и только те из них, взаимодействия с которыми можно наблюдать извне (SMTP-сервер, шина сообщений и т. Д.). Не высмеивайте базу данных, это деталь реализации. Подробнее об этом здесь: enterprisecraftsmanship.com/posts/when-to-mock
Владимир

Ответы:

122

Модульный тест должен проверять один путь кода с помощью одного метода. Когда выполнение метода передается за пределы этого метода, в другой объект и обратно, у вас появляется зависимость.

Когда вы тестируете этот путь кода с фактической зависимостью, вы не тестируете модуль; Вы тестирование интеграции. Хотя это хорошо и необходимо, это не модульное тестирование.

Если в вашей зависимости есть ошибки, это может повлиять на ваш тест и вернуть ложное срабатывание. Например, вы можете передать зависимости неожиданное значение NULL, и зависимость может не передавать значение NULL, как это описано в документации. Ваш тест не объявляет исключение с нулевым аргументом, как должно быть, и тест проходит.

Кроме того, вам может быть сложно, если не невозможно, надежно заставить зависимый объект возвращать именно то, что вы хотите во время теста. Это также включает выдачу ожидаемых исключений в тестах.

Насмешка заменяет эту зависимость. Вы устанавливаете ожидания для вызовов зависимого объекта, устанавливаете точные возвращаемые значения, которые он должен дать вам для выполнения требуемого теста, и / или какие исключения генерировать, чтобы вы могли протестировать свой код обработки исключений. Таким образом, вы можете легко протестировать рассматриваемое устройство.

TL; DR: макет каждой зависимости, к которой относится ваш модульный тест.

Дрю Стивенс
источник
165
Этот ответ слишком радикальный. Модульные тесты могут и должны использовать более одного метода, если все они принадлежат одной связной единице. В противном случае потребовалось бы слишком много насмешек, что привело бы к сложным и хрупким испытаниям. Только зависимости, которые на самом деле не относятся к тестируемому модулю, должны быть заменены с помощью насмешек.
Рожерио
10
Этот ответ также слишком оптимистичен. Было бы лучше, если бы он включал недостатки @ Ян из ложных объектов.
Джефф Аксельрод
1
Разве это не аргумент в пользу введения зависимостей для тестов, а не специально для насмешек? В своем ответе вы можете в значительной степени заменить «макет» на «заглушку». Я согласен с тем, что вы должны либо имитировать, либо заглушить важные зависимости. Я видел много макетов, которые в основном переопределяют части макетов объектов; издевательства, конечно, не серебряная пуля.
Draemon
2
Смоделируйте каждую зависимость, которой касается ваш модульный тест. Это все объясняет.
Теоман Шипахи
2
TL; DR: имитируйте каждую зависимость, которой касается ваш модульный тест. - это не лучший подход, - говорит сам mockito - не смейтесь над всем. (проголосовано против)
p_champ
167

Ложные объекты полезны, когда вы хотите проверить взаимодействие между тестируемым классом и конкретным интерфейсом.

Например, мы хотим проверить, что этот метод 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что ничего не делает. Таким образом, вы можете изменить его один раз для всех тестов, и ваши тесты не будут загромождены длинными настройками, в которых вы тренируете свои заглушки.

В общем, фиктивные объекты имеют свое применение, но, когда они не используются осторожно, они часто поощряют плохие практики, тестируют детали реализации, затрудняют рефакторинг и производят трудные для чтения и сложные в обслуживании тесты .

Дополнительные сведения о недостатках макетов см. Также в разделе « Объекты-макеты: недостатки и варианты использования» .

Ян Солтис
источник
1
Хорошо продуманный ответ, и я в основном согласен. Я бы сказал, что, поскольку модульные тесты представляют собой тестирование методом белого ящика, необходимость изменения тестов при изменении реализации для отправки более привлекательных PDF-файлов не может быть необоснованным бременем. Иногда макеты могут быть полезным способом быстро реализовать заглушки вместо большого количества шаблонов. Однако на практике кажется, что их использование не ограничивается этими простыми случаями.
Draemon
1
Разве весь смысл макета не в том, что ваши тесты согласованы, что вам не нужно беспокоиться о насмешках над объектами, чьи реализации постоянно меняются, возможно, другими программистами каждый раз, когда вы запускаете свой тест и получаете согласованные результаты теста?
PositiveGuy
1
Очень хорошие и актуальные вопросы (особенно о тестах на хрупкость). Раньше я много использовал моки, когда был моложе, но теперь я считаю модульный тест, который сильно зависит от моков, потенциально одноразовым и больше фокусируюсь на интеграционном тестировании (с реальными компонентами)
Kemoda
6
«Невозможность использовать класс в тестах обычно указывает на некоторые проблемы с классом». Если класс является сервисом (например, доступ к базе данных или прокси к веб-сервису), его следует рассматривать как внешнюю зависимость и имитировать / заглушать
Майкл Фрейдгейм
1
Но что произойдет позже, когда мы изменим sendInvitations ()? Если тестируемый код изменяется, это больше не гарантирует предыдущий контракт, следовательно, он должен потерпеть неудачу. И обычно в таких ситуациях не проходит не только один тест . Если это так, то код реализован не полностью. Проверка вызовов методов зависимости должна быть проверена только один раз (в соответствующем модульном тесте). Все остальные классы будут использовать только фиктивный экземпляр. Так что я не вижу никаких преимуществ в сочетании интеграции с модульными тестами.
Кристофер Уилл
55

Практическое правило:

Если для функции, которую вы тестируете, требуется сложный объект в качестве параметра, и было бы сложно просто создать экземпляр этого объекта (если, например, он пытается установить TCP-соединение), используйте макет.

Орион Эдвардс
источник
4

Вы должны смоделировать объект, когда у вас есть зависимость в блоке кода, который вы пытаетесь проверить, который должен быть «просто так».

Например, когда вы пытаетесь протестировать некоторую логику в своей единице кода, но вам нужно получить что-то от другого объекта, и то, что возвращается из этой зависимости, может повлиять на то, что вы пытаетесь протестировать - имитируйте этот объект.

Отличный подкаст на эту тему можно найти здесь

Торан Биллапс
источник
Ссылка теперь перенаправляет на текущий эпизод, а не на предполагаемый эпизод. Предполагаемый подкаст - это hanselminutes.com/32/mock-objects ?
С Перкинс