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

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

Ответ 1

A unit test должен протестировать одну кодировку одним методом. Когда выполнение метода выходит за пределы этого метода, в другой объект и обратно, у вас есть зависимость.

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

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

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

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

TL; DR: Макет каждой зависимости, к которой прикасается unit test.

Ответ 2

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

Например, мы хотим проверить, что метод sendInvitations(MailServer mailServer) вызывает MailServer.createMessage() ровно один раз, а также вызывает MailServer.sendMessage(m) ровно один раз, и никакие другие методы не вызываются в интерфейсе MailServer. Это когда мы можем использовать макет объектов.

С помощью mock-объектов вместо передачи реального 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, первый тест отлично справляется с тем, что мы все еще правильно сортируем, что является целым рядом тестов - они позволяют нам безопасно изменять код. С другой стороны, последний тест всегда ломается, и он активно вреден; это препятствует рефакторингу.

Mocks как заглушки

Макетные рамки часто позволяют также менее строгое использование, где нам не нужно точно указывать, сколько раз нужно вызывать методы и какие параметры ожидаются; они позволяют создавать макетные объекты, которые используются как stubs.

Предположим, что мы имеем метод 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. Если это невозможно, измените реальные классы, чтобы сделать это возможным. Невозможность использовать класс в тестах обычно указывает на некоторые проблемы с классом. Устранение проблем - беспроигрышная ситуация - вы исправили класс, и у вас более простой тест. С другой стороны, не исправление и использование mocks - это беспроигрышная ситуация - вы не исправили настоящий класс, и у вас есть более сложные, менее читаемые тесты, которые препятствуют дальнейшим рефакторингам.
  • Попробуйте создать простую тестовую реализацию интерфейса, а не издеваться над ним в каждом тесте и использовать этот тестовый класс во всех своих тестах. Создайте TestPdfFormatter, который ничего не делает. Таким образом, вы можете изменить его один раз для всех тестов, и ваши тесты не загромождают длинными настройками, где вы тренируете свои заглушки.

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

Более подробную информацию о недостатках mocks см. также Mock Objects: Недостатки и случаи использования.

Ответ 3

Общее правило:

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

Ответ 4

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

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

Большой подкаст по этой теме можно найти здесь