Есть ли смысл в модульных тестах, которые заглушают и издеваются над всем?


59

Когда я выполняю модульные тесты «правильным» способом, то есть заглушаем каждый публичный вызов и возвращаем предустановленные значения или макеты, я чувствую, что на самом деле ничего не тестирую. Я буквально смотрю на свой код и создаю примеры, основанные на потоке логики через мои публичные методы. И каждый раз, когда меняется реализация, я должен идти и менять эти тесты, опять же, не чувствуя, что я выполняю что-то полезное (будь то в среднесрочной или долгосрочной перспективе). Я также провожу интеграционные тесты (в том числе не-счастливый путь), и я не против увеличения времени тестирования. С ними я чувствую, что на самом деле тестирую регрессии, потому что они поймали несколько, в то время как все, что делают модульные тесты, показывает мне, что изменилась реализация моего открытого метода, что я уже знаю.

Модульное тестирование - обширная тема, и я чувствую, что я здесь чего-то не понимаю. В чем заключается решающее преимущество модульного тестирования перед интеграционным (без учета временных затрат)?


4
Мои два цента: не злоупотребляйте издевательствами ( googletesting.blogspot.com/2013/05/…
Хуан Мендес,

Ответы:


37

Когда я выполняю модульные тесты «правильным» способом, то есть заглушаем каждый публичный вызов и возвращаем предустановленные значения или макеты, я чувствую, что на самом деле ничего не тестирую. Я буквально смотрю на свой код и создаю примеры, основанные на потоке логики через мои публичные методы.

Это похоже на то, что тестируемому методу нужны несколько других экземпляров класса (которые вы должны смоделировать), и он вызывает несколько методов сам по себе.

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

Что я нашел полезным, это разделить такие классы на:

  1. Занятия с актуальной «бизнес-логикой». Они используют мало вызовов или вообще не обращаются к другим классам и их легко проверить (значения в стоимости вне).
  2. Классы, которые взаимодействуют с внешними системами (файлы, базы данных и т. Д.). Они обертывают внешнюю систему и предоставляют удобный интерфейс для ваших нужд.
  3. Классы, которые "связывают все вместе"

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

Классы из 2. и 3. обычно не могут быть осмысленно проверены юнитами (потому что они не делают ничего полезного сами по себе, они просто «склеивают» код). OTOH, эти классы, как правило, относительно просты (и немногие), поэтому они должны быть адекватно охвачены интеграционными тестами.


Пример

Один урок

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

Если у вас есть все это в одном классе, вам нужно будет вызывать функции БД, которые трудно подделать. В псевдокоде:

1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database

Все три шага потребуют доступа к БД, поэтому много (сложных) насмешек, которые могут сломаться, если код или структура БД изменятся.

Разделить

Вы делитесь на три класса: PriceCalculation, PriceRepository, App.

PriceCalculation выполняет только фактические расчеты и получает необходимые значения. Приложение связывает все вместе:

App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices

Туда:

  • PriceCalculation заключает в себе «бизнес-логику». Это легко проверить, потому что он ничего не вызывает сам по себе.
  • PriceRepository может быть проверен псевдо-модулем, настроив фиктивную базу данных и протестировав вызовы read и update. В нем мало логики, следовательно, мало путей к кодам, поэтому вам не нужно слишком много этих тестов.
  • Приложение не может быть проверено модульно, потому что это клей-код. Однако это тоже очень просто, поэтому тестирования интеграции должно быть достаточно. Если позднее приложение станет слишком сложным, вы раскроете больше классов «бизнес-логики».

Наконец, может оказаться, что PriceCalculation должен выполнять собственные вызовы базы данных. Например, потому что только PriceCalculation знает, какие данные ему нужны, поэтому они не могут быть предварительно выбраны приложением. Затем вы можете передать ему экземпляр PriceRepository (или некоторый другой класс репозитория), настроенный в соответствии с потребностями PriceCalculation. Затем этот класс нужно будет смоделировать, но это будет просто, потому что интерфейс PriceRepository прост, например PriceRepository.getPrice(articleNo, contractType). Наиболее важно то, что интерфейс PriceRepository изолирует PriceCalculation от базы данных, поэтому изменения в схеме БД или организации данных вряд ли изменят его интерфейс и, следовательно, нарушат макеты.


5
Я думал, что я был один, не видя смысла в модульном тестировании всего, спасибо
enthrops

4
Я просто не согласен, когда вы говорите, что классов типа 3 немного, я чувствую, что большая часть моего кода имеет тип 3 и почти нет бизнес-логики. Вот что я имею в виду: stackoverflow.com/questions/38496185/…
Родриго Руис

27

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

Это ложная дихотомия.

Модульное тестирование и интеграционное тестирование служат двум схожим, но различным целям. Цель модульного тестирования - убедиться, что ваши методы работают. С практической точки зрения, модульные тесты удостоверяются в том, что код соответствует контракту, указанному в модульных тестах. Это очевидно в том, как создаются модульные тесты: они конкретно указывают, что должен делать код, и утверждают, что код делает это.

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

Однако, если есть решающее преимущество для модульных тестов, они заключаются в следующем: настроить модульные тесты гораздо проще, и они требуют гораздо меньше времени и усилий, чем интеграционные тесты. При правильном использовании модульные тесты способствуют разработке «тестируемого» кода, что означает, что конечный результат будет более надежным, более понятным и более легким в обслуживании. Тестируемый код обладает определенными характеристиками, такими как целостный API, повторяемое поведение, и он возвращает результаты, которые легко утверждать.

Интеграционные тесты являются более сложными и более дорогостоящими, потому что вам часто требуются сложные макеты, сложные настройки и сложные утверждения. На самом высоком уровне системной интеграции представьте, что вы пытаетесь смоделировать взаимодействие человека в пользовательском интерфейсе. Целые программные системы предназначены для такого рода автоматизации. И мы стремимся к автоматизации; человеческое тестирование не повторяется и не масштабируется как автоматическое тестирование.

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


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

Если вы тратите много времени на написание модульных тестов для тривиального кода, такого как

public string SomeProperty { get; set; }

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

public string SomeMethod(string someProperty);

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


2
Мне известно, что сервер модульного тестирования и интеграционного тестирования преследует разные цели, однако я до сих пор не понимаю, насколько полезны модульные тесты, если вы заглушаете и высмеиваете все публичные вызовы, которые выполняют модульные тесты. Я бы понял, что «код выполняет контракт, изложенный в модульных тестах», если бы не кодовые заглушки; мои модульные тесты буквально отражают логику внутри методов, которые я тестирую. Вы (я) на самом деле ничего не тестируете, просто смотрите на свой код и «конвертируете» его в тесты. Что касается сложности автоматизации и покрытия кода, я в настоящее время занимаюсь Rails, и оба хорошо позаботились.
энтропс

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

2
Имеет смысл, но все же придется заглушить все другие публичные вызовы (db, некоторые «глобальные переменные», такие как текущий статус пользователя и т. Д.) И в конечном итоге протестировать код в соответствии с логикой метода.
энтропс

1
Итак, я полагаю, что модульные тесты предназначены для в основном изолированного материала, который как бы подтверждает «набор входов -> набор ожидаемых результатов»?
Энтропс

1
Мой опыт создания большого количества модульных и интеграционных тестов (не говоря уже о расширенных инструментах моделирования, интеграции и тестирования кода, используемых этими тестами) противоречит большинству ваших утверждений здесь: 1) «Цель модульного тестирования состоит в том, чтобы убедиться, что код делает то, что должен »: то же самое относится и к интеграционному тестированию (тем более); 2) «модульные тесты гораздо проще в настройке»: нет, это не так (довольно часто проще проводить интеграционные тесты); 3) «При правильном использовании модульные тесты способствуют разработке« тестируемого »кода»: то же самое с интеграционными тестами; (продолжение)
Rogério

4

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

Вы также тестируете поведение тестируемого класса, а не реализацию. Если вы реорганизуете код (создавая новые внутренние методы и т. Д.), Модульные тесты не должны проваливаться. Но если вы изменяете то, что делает публичный метод, тогда тесты должны провалиться, потому что вы изменили поведение.

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

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

Модульные тесты помогли мне в разработке, так как я могу сосредоточиться на одной части приложения за раз. Дразнить компоненты, которые я еще не сделал. Они также отлично подходят для регрессии, поскольку они документируют любые ошибки в логике, которую я обнаружил (даже в модульных тестах).

ОБНОВИТЬ

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

Тест для этого метода может никогда не измениться или может измениться, только когда вы меняете методы. Почему это плохо? Это помогает усилить использование тестов. Если вам нужно исправить тест после изменения кода, у вас появится привычка менять тесты с помощью кода.


И если метод не вызывает ничего приватного, то нет смысла в модульном тестировании, верно?
энтропс

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

Нет, я имею в виду, если публичный метод не вызывает ничего приватного, имеет ли смысл тестировать этот публичный метод?
энтропс

Да. Метод что-то делает, не так ли? Так что это должно быть проверено. С точки зрения тестирования, я не знаю, использует ли это что-то личное. Я просто знаю, что если я предоставлю ввод A, я должен получить вывод B.
Schleis

О, да, метод что-то делает, и это что-то - вызовы других открытых методов (и только этого). Таким образом, способ, которым вы «правильно» протестируете, заключается в том, чтобы заглушить вызовы с некоторыми возвращаемыми значениями, а затем настроить ожидания сообщений. Что именно вы тестируете в этом случае? Что правильные звонки сделаны? Ну, вы написали этот метод, и вы можете посмотреть на него и увидеть, что именно он делает. Я думаю, что модульное тестирование больше подходит для изолированных методов, которые должны использоваться, например, «input -> output», поэтому вы можете создать множество примеров, а затем выполнить регрессионное тестирование при рефакторинге.
энтропс

3

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

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

На самом деле, чрезмерная насмешка является анти-паттерном: TDD-анти-паттерны и издевательства - это зло .


0

Хотя операция уже помечена ответом, я просто добавляю свои 2 цента здесь.

В чем заключается решающее преимущество модульного тестирования перед интеграционным (без учета временных затрат)?

А также в ответ на

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

Есть полезный, но не совсем то, что спросил ОП:

Модульные тесты работают, но все еще есть ошибки?

Исходя из моего небольшого опыта в тестировании комплектов, я понимаю, что модульные тесты всегда должны тестировать самые базовые функциональные возможности уровня метода класса. На мой взгляд, каждый метод, публичный, частный или внутренний, заслуживает отдельного модульного тестирования. Даже в моем недавнем опыте у меня был публичный метод, который вызывал другой маленький частный метод. Итак, было два подхода:

  1. не создавайте модульные тесты для частного метода.
  2. создать модульный тест (ы) для частного метода.

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


Рассматривается случай, когда закрытый метод используется многими другими публичными методами.

Итак, если бы я выбрал подход 1: я бы дублировал модульные тесты, и они были бы сложными, так как у вас было количество модульных тестов для каждой ветви открытого метода, а также частного метода.

Если бы я выбрал подход 2: кода, написанного для модульных тестов, было бы относительно меньше, и было бы намного проще тестировать.


Рассматривая случай, когда закрытый метод не используется повторно Нет смысла писать отдельный модульный тест для этого метода.

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.