Когда я выполняю модульные тесты «правильным» способом, то есть заглушаем каждый публичный вызов и возвращаем предустановленные значения или макеты, я чувствую, что на самом деле ничего не тестирую. Я буквально смотрю на свой код и создаю примеры, основанные на потоке логики через мои публичные методы. И каждый раз, когда меняется реализация, я должен идти и менять эти тесты, опять же, не чувствуя, что я выполняю что-то полезное (будь то в среднесрочной или долгосрочной перспективе). Я также провожу интеграционные тесты (в том числе не-счастливый путь), и я не против увеличения времени тестирования. С ними я чувствую, что на самом деле тестирую регрессии, потому что они поймали несколько, в то время как все, что делают модульные тесты, показывает мне, что изменилась реализация моего открытого метода, что я уже знаю.
Модульное тестирование - обширная тема, и я чувствую, что я здесь чего-то не понимаю. В чем заключается решающее преимущество модульного тестирования перед интеграционным (без учета временных затрат)?
источник
Ответы:
Это похоже на то, что тестируемому методу нужны несколько других экземпляров класса (которые вы должны смоделировать), и он вызывает несколько методов сам по себе.
Этот тип кода действительно сложен для модульного тестирования по причинам, которые вы изложили.
Что я нашел полезным, это разделить такие классы на:
Тогда классы из 1. легко поддаются модульному тестированию, потому что они просто принимают значения и возвращают результат. В более сложных случаях этим классам может потребоваться выполнять вызовы самостоятельно, но они будут вызывать только классы из 2. (а не напрямую вызывать, например, функцию базы данных), а классы из 2. легко подделать (потому что они только выставить те части обернутой системы, которые вам нужны).
Классы из 2. и 3. обычно не могут быть осмысленно проверены юнитами (потому что они не делают ничего полезного сами по себе, они просто «склеивают» код). OTOH, эти классы, как правило, относительно просты (и немногие), поэтому они должны быть адекватно охвачены интеграционными тестами.
Пример
Один урок
Допустим, у вас есть класс, который извлекает цену из базы данных, применяет некоторые скидки и затем обновляет базу данных.
Если у вас есть все это в одном классе, вам нужно будет вызывать функции БД, которые трудно подделать. В псевдокоде:
Все три шага потребуют доступа к БД, поэтому много (сложных) насмешек, которые могут сломаться, если код или структура БД изменятся.
Разделить
Вы делитесь на три класса: PriceCalculation, PriceRepository, App.
PriceCalculation выполняет только фактические расчеты и получает необходимые значения. Приложение связывает все вместе:
Туда:
Наконец, может оказаться, что PriceCalculation должен выполнять собственные вызовы базы данных. Например, потому что только PriceCalculation знает, какие данные ему нужны, поэтому они не могут быть предварительно выбраны приложением. Затем вы можете передать ему экземпляр PriceRepository (или некоторый другой класс репозитория), настроенный в соответствии с потребностями PriceCalculation. Затем этот класс нужно будет смоделировать, но это будет просто, потому что интерфейс PriceRepository прост, например
PriceRepository.getPrice(articleNo, contractType)
. Наиболее важно то, что интерфейс PriceRepository изолирует PriceCalculation от базы данных, поэтому изменения в схеме БД или организации данных вряд ли изменят его интерфейс и, следовательно, нарушат макеты.источник
Это ложная дихотомия.
Модульное тестирование и интеграционное тестирование служат двум схожим, но различным целям. Цель модульного тестирования - убедиться, что ваши методы работают. С практической точки зрения, модульные тесты удостоверяются в том, что код соответствует контракту, указанному в модульных тестах. Это очевидно в том, как создаются модульные тесты: они конкретно указывают, что должен делать код, и утверждают, что код делает это.
Интеграционные тесты разные. Интеграционные тесты осуществляют взаимодействие между программными компонентами. У вас могут быть программные компоненты, которые проходят все свои тесты и по-прежнему не проходят интеграционные тесты, потому что они не взаимодействуют должным образом.
Однако, если есть решающее преимущество для модульных тестов, они заключаются в следующем: настроить модульные тесты гораздо проще, и они требуют гораздо меньше времени и усилий, чем интеграционные тесты. При правильном использовании модульные тесты способствуют разработке «тестируемого» кода, что означает, что конечный результат будет более надежным, более понятным и более легким в обслуживании. Тестируемый код обладает определенными характеристиками, такими как целостный API, повторяемое поведение, и он возвращает результаты, которые легко утверждать.
Интеграционные тесты являются более сложными и более дорогостоящими, потому что вам часто требуются сложные макеты, сложные настройки и сложные утверждения. На самом высоком уровне системной интеграции представьте, что вы пытаетесь смоделировать взаимодействие человека в пользовательском интерфейсе. Целые программные системы предназначены для такого рода автоматизации. И мы стремимся к автоматизации; человеческое тестирование не повторяется и не масштабируется как автоматическое тестирование.
Наконец, интеграционное тестирование не дает никаких гарантий относительно покрытия кода. Сколько комбинаций циклов, условий и веток кода вы тестируете с помощью интеграционных тестов? Вы действительно знаете? Существуют инструменты, которые вы можете использовать с юнит-тестами и тестируемыми методами, которые подскажут вам, какой объем кода у вас есть, и какова цикломатическая сложность вашего кода. Но они действительно хорошо работают только на уровне методов, где живут модульные тесты.
Если ваши тесты меняются каждый раз, когда вы выполняете рефакторинг, это другая проблема. Под модульными тестами подразумевается документирование того, что делает ваше программное обеспечение, подтверждение того, что оно делает это, а затем подтверждение того, что оно делает это снова, когда вы реорганизуете базовую реализацию. Если ваш API меняется, или вам нужно, чтобы ваши методы изменились в соответствии с изменением структуры системы, это то, что должно произойти. Если это часто происходит, подумайте о написании тестов, прежде чем писать код. Это заставит вас задуматься об общей архитектуре и позволит вам писать код с уже установленным API.
Если вы тратите много времени на написание модульных тестов для тривиального кода, такого как
тогда вы должны пересмотреть свой подход. Модульное тестирование должно проверять поведение, и в строке кода выше нет поведения. Однако вы где-то создали зависимость в своем коде, поскольку это свойство почти наверняка будет упоминаться в другом месте вашего кода. Вместо этого рассмотрим написание методов, которые принимают необходимое свойство в качестве параметра:
Теперь ваш метод не имеет каких-либо зависимостей от чего-то вне его, и теперь он более тестируемый, поскольку он полностью автономен. Конечно, вы не всегда сможете это сделать, но это действительно продвигает ваш код в сторону большей тестируемости, и на этот раз вы пишете модульный тест для реального поведения.
источник
Модульные тесты с макетами предназначены для проверки правильности реализации класса. Вы издеваетесь над открытыми интерфейсами зависимостей кода, который вы тестируете. Таким образом, вы контролируете все, что является внешним по отношению к классу, и уверены, что провальный тест вызван чем-то внутренним для класса, а не каким-либо другим объектом.
Вы также тестируете поведение тестируемого класса, а не реализацию. Если вы реорганизуете код (создавая новые внутренние методы и т. Д.), Модульные тесты не должны проваливаться. Но если вы изменяете то, что делает публичный метод, тогда тесты должны провалиться, потому что вы изменили поведение.
Это также звучит так, как будто вы пишете тесты после написания кода, попробуйте вместо этого написать первые тесты. Попробуйте описать поведение, которое должен иметь класс, а затем напишите минимальный объем кода, чтобы тесты прошли.
И модульное тестирование, и интеграционное тестирование полезны для обеспечения качества вашего кода. В модульных тестах каждый компонент изучается изолированно. А интеграционные тесты обеспечивают правильное взаимодействие всех компонентов. Я хочу, чтобы оба типа были в моем наборе тестов.
Модульные тесты помогли мне в разработке, так как я могу сосредоточиться на одной части приложения за раз. Дразнить компоненты, которые я еще не сделал. Они также отлично подходят для регрессии, поскольку они документируют любые ошибки в логике, которую я обнаружил (даже в модульных тестах).
ОБНОВИТЬ
Создание теста, который только проверяет, что вызываемые методы имеют значение, означает, что вы действительно вызываете методы. В частности, если вы сначала пишете свои тесты, у вас есть контрольный список методов, которые должны произойти. Поскольку этот код в значительной степени процедурный, вам не нужно много проверять, кроме как вызывать методы. Вы защищаете код для изменения в будущем. Когда вам нужно вызвать один метод перед другим. Или что метод всегда вызывается, даже если исходный метод вызывает исключение.
Тест для этого метода может никогда не измениться или может измениться, только когда вы меняете методы. Почему это плохо? Это помогает усилить использование тестов. Если вам нужно исправить тест после изменения кода, у вас появится привычка менять тесты с помощью кода.
источник
У меня был похожий вопрос - пока я не обнаружил силу тестов компонентов. Короче говоря, они такие же, как модульные тесты, за исключением того, что по умолчанию вы не имитируете, а используете реальные объекты (в идеале через внедрение зависимостей).
Таким образом, вы можете быстро создавать надежные тесты с хорошим охватом кода. Нет необходимости постоянно обновлять свои макеты. Он может быть немного менее точным, чем юнит-тесты со 100% -ными мошенничествами, но сэкономленное время и деньги компенсируют это Единственное, для чего вам действительно нужно использовать макеты или приспособления, это серверные хранилища или внешние сервисы.
На самом деле, чрезмерная насмешка является анти-паттерном: TDD-анти-паттерны и издевательства - это зло .
источник
Хотя операция уже помечена ответом, я просто добавляю свои 2 цента здесь.
А также в ответ на
Есть полезный, но не совсем то, что спросил ОП:
Модульные тесты работают, но все еще есть ошибки?
Исходя из моего небольшого опыта в тестировании комплектов, я понимаю, что модульные тесты всегда должны тестировать самые базовые функциональные возможности уровня метода класса. На мой взгляд, каждый метод, публичный, частный или внутренний, заслуживает отдельного модульного тестирования. Даже в моем недавнем опыте у меня был публичный метод, который вызывал другой маленький частный метод. Итак, было два подхода:
Если вы думаете логически, смысл использования частного метода таков: основной открытый метод становится слишком большим или грязным. Чтобы решить эту проблему, Вы мудро рефакторизируете и создаете небольшие куски кода, которые заслуживают отдельного частного метода, что, в свою очередь, делает Ваш основной публичный метод менее громоздким. Вы выполняете рефакторинг, помня, что этот закрытый метод может быть использован позже. Могут быть случаи, когда нет другого открытого метода, зависящего от этого частного метода, но который знает о будущем.
Рассматривается случай, когда закрытый метод используется многими другими публичными методами.
Итак, если бы я выбрал подход 1: я бы дублировал модульные тесты, и они были бы сложными, так как у вас было количество модульных тестов для каждой ветви открытого метода, а также частного метода.
Если бы я выбрал подход 2: кода, написанного для модульных тестов, было бы относительно меньше, и было бы намного проще тестировать.
Рассматривая случай, когда закрытый метод не используется повторно Нет смысла писать отдельный модульный тест для этого метода.
Что касается интеграционных тестов , то они, как правило, носят исчерпывающий характер и более высокого уровня. Они скажут Вам, что с учетом полученных данных все Ваши классы должны прийти к этому окончательному выводу. Чтобы узнать больше о полезности интеграционного тестирования, пожалуйста, смотрите упомянутую ссылку.
источник