Когда я должен издеваться? - PullRequest
112 голосов
/ 01 сентября 2008

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

Ответы [ 4 ]

141 голосов
/ 01 сентября 2008

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

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

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

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

110 голосов
/ 01 сентября 2008

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

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

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

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

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

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

46 голосов
/ 02 сентября 2008

Правило большого пальца:

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

3 голосов
/ 01 сентября 2008

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

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

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

...