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

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


person Esteban Araya    schedule 01.09.2008    source источник
comment
Я рекомендую имитировать только внепроцессные зависимости и только те из них, взаимодействия с которыми наблюдаются извне (SMTP-сервер, шина сообщений и т. Д.). Не высмеивайте базу данных, это деталь реализации. Подробнее об этом здесь: enterprisecraftsmanship.com/posts/when-to-mock   -  person Vladimir    schedule 19.04.2020


Ответы (4)


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

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

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

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

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

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

person Community    schedule 01.09.2008
comment
Этот ответ слишком радикален. Модульные тесты могут и должны использовать более одного метода, если все они принадлежат одной и той же связной единице. В противном случае потребовалось бы слишком много насмешек / подделок, что привело бы к сложным и ненадежным тестам. Только зависимости, которые на самом деле не принадлежат тестируемому модулю, должны быть заменены посредством имитации. - person Rogério; 26.08.2010
comment
Этот ответ тоже слишком оптимистичен. Было бы лучше, если бы он включал в себя недостатки @Jan, связанные с фиктивными объектами. - person Jeff Axelrod; 26.05.2011
comment
Разве это не аргумент в пользу внедрения зависимостей для тестов, а не специально для имитаций? Вы могли бы в своем ответе заменить mock на заглушку. Я согласен с тем, что вы должны либо имитировать, либо заглушить важные зависимости. Я видел много ложно-тяжелого кода, который в основном заканчивается переопределением частей имитируемых объектов; издевательства, конечно, не серебряная пуля. - person Draemon; 24.03.2012
comment
@ Rogério Нет, если у вас хорошо спроектированная система: destroyallsoftware.com/blog/2014/ - person weberc2; 14.12.2014
comment
@ Rogério Edit: я понял, что мы всегда должны создавать экземпляры зависимостей (т.е. каждый модульный тест также является потенциальным интеграционным тестом); возможно, вы имели в виду насмешку, а не заглушку (некоторые зависимости можно заглушить, а не заглушать)? - person weberc2; 14.12.2014
comment
@ weberc2 Я предпочитаю использовать mock в качестве общего термина как для строгих mock, так и для stubs, поскольку часто возникает путаница и различное понимание их (например, некоторые думают, что mock всегда требует, чтобы все ожидания были записаны, что верно только для строгие насмешки). Но суть здесь в том, что часто у нас есть зависимости, которые попадают внутрь тестируемого модуля; лично мне нравится создавать непубличные вспомогательные классы в одном пакете (Java) всякий раз, когда публичный класс становится слишком большим или сложным; такие зависимости на самом деле являются внутренними деталями, и я не тестирую их отдельно. - person Rogério; 15.12.2014
comment
@ Rogério Я думаю, ты путаешь единицу с классом. В этом случае ваш частный вспомогательный класс принадлежит к тому же модулю, что и класс, которому он помогает. Это не внешняя зависимость. Этот пример не противоречит ответу, он его поддерживает. - person weberc2; 15.12.2014
comment
@ weberc2 В вашем ответе говорится только о зависимости, без указания того, является ли она внутренней или внешней по отношению к тестируемому модулю. Итак, я предположил, что это означает любую зависимость модуля. Если на самом деле это означает только внешнюю зависимость, то мы согласны. Но ответ сбивает с толку, когда в нем говорится, что один кодовый путь через один метод, поскольку у нас, очевидно, будет несколько кодовых путей через несколько методов, все внутри модуля. - person Rogério; 15.12.2014
comment
@ Рожерио, это не мой ответ. Что еще более важно, насколько мне известно, зависимости могут быть только внешними по отношению к модулю. То, что вы называете внутренней зависимостью, не является зависимостью от вашего модуля, это часть вашего модуля. Таким образом, ответ не противоречит вашему сценарию. - person weberc2; 15.12.2014
comment
Смоделируйте каждую зависимость, которой касается ваш модульный тест. Это все объясняет. - person Teoman shipahi; 21.01.2015
comment
Я предполагаю, что это одна из общепринятых философий модульного тестирования ... Сомнительная (или, по крайней мере, ограниченная сфера применения), но эй, выбейте себя. Что худшего могло случиться? :) - person kayleeFrye_onDeck; 26.03.2016
comment
Это может быть чрезвычайно неприятная задача проводить модульное тестирование таким образом, но это, безусловно, лучший из возможных подходов к ней. - person Wes; 03.02.2017
comment
TL; DR: имитируйте каждую зависимость, которой касается ваш модульный тест. - это не самый лучший подход, - говорит сам mockito - не смейтесь над всем. (проголосовано против) - person p_champ; 08.01.2019
comment
Я думаю, что ключевая фраза, которую некоторые люди упустили в этом ответе: когда выполнение метода переходит за пределы этого метода, в другой объект и обратно, у вас есть зависимость. метод в том же классе. Я согласен с тем, что когда задействован другой объект, это интеграционный тест, а не модульный тест. - person Bob Ray; 21.02.2020
comment
Я не хочу создавать новый вопрос. Спрошу здесь. Представьте, что вызов метода имитируемого объекта возвращает некоторый класс-оболочку с базовым установщиком и получателем. Также представьте, что эта оболочка определена в другой библиотеке. Вы все еще создадите макет для этого класса-оболочки? - person user1415536; 14.09.2020
comment
Издевательство над всем вокруг снова дает эффект ложных срабатываний. Мокинг также избавляет от множества взаимодействий кода, при которых ваше приложение может выйти из строя во время выполнения в PROD. Также сложно поддерживать каскадное мокирование. - person Traycho Ivanov; 04.11.2020
comment
@TraychoIvanov См. arialdomartini. wordpress.com/2011/10/21/ - person Leponzo; 29.03.2021
comment
@Leponzo Звучит неплохо, но практика рассказывает нам разные истории. Насмешка над каждой зависимостью означает, что вам нужно знать, как работает зависимость, особенно если она вложенная. Имитация всего вашего кода и использование только этих тестов порождает множество неожиданных проблем с PROD. Должен быть баланс, и статья не могла бы его подробно объяснить. Я не любитель издеваться над всем. - person Traycho Ivanov; 01.04.2021
comment
@TraychoIvanov Да, поэтому вам нужны и интеграция, и модульные тесты. В статье просто говорится, что ваши модульные тесты должны имитировать все зависимости. - person Leponzo; 01.04.2021
comment
@Leponzo, это правда, но у меня сложилось впечатление, что вы не работали с большей кодовой базой, где имитировать все невозможно, поскольку зависимости имеют несколько уровней вложенности, и насмешка над ними кажется очень плохой вещью, особенно, когда они регулярно реорганизуются. Теория и практика - две очень разные вещи. Есть много статей, не поддерживающих псевдоним. Я бы посоветовал рассматривать каждый случай отдельно, золотого правила не существует. techyourchance.com/mocks-in-unit-testing, jtway.co/mock-everything-is-a-good-way -to-раковина-b8a1284fb81f - person Traycho Ivanov; 01.04.2021

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

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

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

Для получения дополнительной информации о недостатках имитаций см. Также Mock Objects: Insights and Примеры использования.

person Jan Soltis    schedule 01.09.2008
comment
Хорошо продуманный ответ, и я в основном согласен. Я бы сказал, что, поскольку модульные тесты представляют собой тестирование методом белого ящика, необходимость изменения тестов при изменении реализации для отправки более красивых PDF-файлов не может быть необоснованным бременем. Иногда макеты могут быть полезным способом быстро реализовать заглушки вместо большого количества шаблонов. Однако на практике кажется, что их использование не ограничивается этими простыми случаями. - person Draemon; 24.03.2012
comment
Разве весь смысл макета не в том, что ваши тесты согласованы, что вам не нужно беспокоиться о насмешках над объектами, чьи реализации постоянно меняются, возможно, другими программистами каждый раз, когда вы запускаете свой тест и получаете согласованные результаты теста? - person PositiveGuy; 01.04.2013
comment
Очень хорошие и актуальные моменты (особенно о хрупкости тестов). Раньше я много использовал макеты, когда был моложе, но теперь я считаю модульный тест, который сильно зависит от макетов, потенциально одноразовым и больше сосредотачиваюсь на интеграционном тестировании (с реальными компонентами) - person Kemoda; 18.06.2013
comment
Невозможность использовать класс в тестах обычно указывает на некоторые проблемы с классом. Если класс является сервисом (например, доступ к базе данных или прокси к веб-сервису), его следует рассматривать как внешнюю зависимость и имитировать / заглушать - person Michael Freidgeim; 06.07.2013
comment
@MichaelFreidgeim Зачем имитировать базу данных? Почему бы не использовать встроенную базу данных для проверки того же? Таким образом, у вас не будет отдельных модульных и интеграционных тестов. Кроме того, ваши тесты выполняются быстрее по мере необходимости, потому что база данных встроена. - person Narendra Pathai; 16.11.2014
comment
Очень четко выражена зависимость, которую Mock потенциально создает с SUT. - person vibhu; 11.07.2016
comment
Но что произойдет позже, когда мы изменим sendInvitations ()? Если тестируемый код изменится, это больше не гарантирует предыдущий контракт, следовательно, он должен потерпеть неудачу. И обычно не только один тест терпит неудачу в подобных ситуациях. Если это так, значит, код реализован не чисто. Проверка вызовов методов зависимости должна быть протестирована только один раз (в соответствующем модульном тесте). Все остальные классы будут использовать только фиктивный экземпляр. Так что я не вижу никаких преимуществ в сочетании интеграции с юнит-тестами. - person Christopher Will; 26.06.2018
comment
@ChristopherWill Я думаю, что автор считает, что чрезмерное использование имитаторов склонно заманивать разработчиков к тестированию чрезмерно жестких или откровенно ложных контрактов, основанных на нерелевантные детали реализации. Ян указывает, что в тех случаях, когда эти детали являются действительно важными (как в тесте сравнения сортировки), тогда возникает ситуация, когда имитация имеет смысл. - person ggorlen; 15.02.2021

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

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

person Orion Edwards    schedule 01.09.2008

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

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

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

person Toran Billups    schedule 01.09.2008
comment
Ссылка теперь ведет к текущему выпуску, а не к запланированному выпуску. Предполагаемый подкаст - это hanselminutes.com/32/mock-objects? - person C Perkins; 10.06.2020