Вопрос

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

Это было полезно?

Решение

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

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

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

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

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

ТЛ;ДР:Издевайтесь над каждой зависимостью, которой касается ваш модульный тест.

Другие советы

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

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

С фиктивными объектами вместо передачи реального MailServerImpl, или тест TestMailServer, мы можем передать макет реализации MailServer интерфейс.Прежде чем мы проведем издевательство MailServer, мы «обучаем» его, чтобы он знал, какие вызовы метода следует ожидать и какие возвращаемые значения следует возвращать.В конце макетный объект утверждает, что все ожидаемые методы были вызваны как ожидалось.

Теоретически это звучит хорошо, но есть и некоторые недостатки.

Ложные недостатки

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

Вот пример в псевдокоде.Предположим, мы создали MySorter class, и мы хотим его протестировать:

// 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-соединение), используйте макет.

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top