Должны ли мы проверить все наши методы?


62

Поэтому сегодня я поговорил с моим товарищем по команде о модульном тестировании. Все началось, когда он спросил меня: «Привет, где тесты для этого класса, я вижу только один?». Весь класс был менеджером (или службой, если вы предпочитаете называть это так), и почти все методы просто делегировали вещи в DAO, так что это было похоже на:

SomeClass getSomething(parameters) {
    return myDao.findSomethingBySomething(parameters);
}

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

Это заставило меня задуматься. Должны ли мы стремиться к максимальному охвату тестами%? Или это просто искусство ради искусства? Я просто не вижу никакой причины для тестирования таких вещей, как:

  • геттеры и сеттеры (если в них нет логики)
  • "шаблонный" код

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

Существуют ли какие-либо рациональные / не "легковоспламеняющиеся" причины, по которым нужно тестировать каждую (или столько, сколько он может) строку кода?


2
Я все еще решаюсь по этому вопросу, но вот разговор о ком-то, кто решил, что ответ "нет". Йен Купер: TDD, где все пошло не так? Подводя итог этой замечательной лекции, вы должны тестировать снаружи и проверять новое поведение, а не новые методы.
Даниэль Каплан

Это действительно отличный разговор, который нужно увидеть, откровение для многих людей, мне это нравится. Но я думаю, что ответ не "нет". Это «да, но косвенно». Ян Купер рассказывает о гексагональной архитектуре и тестировании функций / поведения, насмешливых / заглушающих порты. В этом случае это порты DAO, и этот «менеджер / сервис» тестировался не с отдельным модульным тестом только для этого класса, а с «модульным тестом» (модулем в определении Яна Купера, с которым я полностью согласен), который тестирует некоторые функции. в вашем домене, которые используют этот менеджер / сервис.
AlfredoCasado


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

Ответы:


49

Я следую эмпирическому правилу Кента Бека:

Проверьте все, что может сломаться.

Конечно, это в некоторой степени субъективно. Для меня тривиальные геттеры / сеттеры и однострочные, как у вас выше, обычно не стоят того. Но опять же, я трачу большую часть своего времени на написание модульных тестов для унаследованного кода, мечтая только о хорошем новом проекте TDD ... В таких проектах правила разные. При использовании унаследованного кода основная цель - охватить как можно больше усилий с минимальными усилиями, поэтому модульные тесты, как правило, более высокого уровня и более сложные, больше похожи на интеграционные тесты, если кто-то педантичен в отношении терминологии. И когда вы боретесь за то, чтобы увеличить общее покрытие кода с 0%, или просто сумели поднять его более чем на 25%, методы получения и установки модульного тестирования - это наименьшее из ваших беспокойств.

OTOH в новом проекте TDD, может быть более логично писать тесты даже для таких методов. Тем более, что вы уже написали тест до того, как у вас появится возможность задуматься: «Стоит ли эта строка для отдельного теста?». По крайней мере, эти тесты тривиальны для написания и быстрого запуска, так что в любом случае это не имеет большого значения.


Ах, я полностью забыл эту цитату! Думаю, я буду использовать его в качестве основного аргумента, потому что, честно говоря - что может сломаться здесь? Не очень много. Единственное, что может сломаться, - это вызов метода, и если это произойдет, это означает, что произошло что-то действительно плохое. Спасибо!
Zenzen

5
@Zenzen: «что может сломаться здесь? Не очень». - Так это может сломаться. Просто небольшая опечатка. Или кто-то добавляет код. Или портит зависимость. Я действительно думаю, что Бек будет утверждать, что ваш главный пример квалифицируется как хрупкий. Геттеры и сеттеры, тем не менее, хотя я даже тогда обнаружил ошибку копирования / вставки. Реальный вопрос в том, что если тест написать слишком тривиально, то почему он вообще существует?
фунтовые

1
Сколько времени вы уже потратили на обдумывание, вы могли бы написать тест. я говорю: пишите тест, не уходите, когда тест не будет записан как серая область, появятся более разбитые окна.
kett_chup

1
Я добавлю, что мой общий опыт заключается в том, что тестирование геттеров и сеттеров несколько полезно в долгосрочной перспективе, но с низким приоритетом. Причина в том, что, поскольку у него теперь «нулевой» шанс на обнаружение ошибки, вы не можете гарантировать, что другой разработчик не добавит в течение трех месяцев что-либо («просто простое утверждение if»), которое может сломаться , Наличие модульного теста защищает от этого. В то же время, это не слишком высокий приоритет, потому что скоро вы не найдете ничего подобного.
dclements

7
Слепое тестирование всего, что может сломаться, не имеет смысла. Должна быть стратегия, при которой компоненты с высоким риском сначала тестируются.
CodeART

13

Существует несколько видов юнит-тестирования:

  • Государственный Вы действуете, а затем утверждаете против состояния объекта. Например, я делаю депозит. Затем я проверяю, увеличился ли баланс.
  • Возвращаемое значение на основе. Вы действуете и утверждаете против возвращаемого значения.
  • Взаимодействие на основе. Вы проверяете, что ваш объект вызвал другой объект. Похоже, это то, что вы делаете в своем примере.

Если бы вы сначала написали свой тест, то это имело бы больше смысла - как вы ожидаете вызвать слой доступа к данным. Сначала тест не пройдёт. Затем вы должны написать производственный код для прохождения теста.

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

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

В настоящее время там нет логики, но так будет не всегда.

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

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

Сказав все это, я бы не стал вслепую проверять все. Я бы установил горячие точки (компоненты с высокой сложностью и высоким риском поломки). Я бы тогда сосредоточился на этих компонентах. Нет смысла иметь кодовую базу, где 90% кодовой базы довольно просты и покрыты модульными тестами, тогда как оставшиеся 10% представляют базовую логику системы и не охватываются модульными тестами из-за их сложности.

Наконец, в чем выгода тестирования этого метода? Каковы последствия, если это не работает? Они катастрофические? Не стремитесь получить высокий охват кода. Покрытие кода должно быть результатом хорошего набора модульных тестов. Например, вы можете написать один тест, который пройдет по дереву и даст вам 100% охват этого метода, или вы можете написать три модульных теста, которые также обеспечат вам 100% покрытие. Разница в том, что при написании трех тестов вы тестируете крайние случаи, а не просто обходите дерево.


Зачем вам проверять, что ваш DAL был вызван только один раз?
Марьян Венема

9

Вот хороший способ подумать о качестве вашего программного обеспечения:

  1. проверка типов является частью проблемы.
  2. тестирование справится с остальными

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


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

6

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


Помните, что некоторые методы получения или установки имеют побочные эффекты (хотя это не рекомендуется и считается плохой практикой в ​​большинстве случаев), поэтому изменения в вашем исходном коде также могут повлиять на него.
Анджей Бобак

3

Громкое ДА с TDD (и с несколькими исключениями)

Спорные хорошо, но я бы утверждать, что тот, кто отвечает «нет» на этот вопрос отсутствует фундаментальное понятие TDD.

Для меня ответ - да, если ты следуешь TDD. Если нет, то нет правдоподобного ответа.

DDD в TDD

Часто упоминается, что TDD имеет основные преимущества.

  • Оборона
    • Обеспечение кода может измениться, но не его поведение .
    • Это позволяет очень важную практику рефакторинга .
    • Вы получаете этот TDD или нет.
  • дизайн
    • Вы указываете, что что-то должно делать, как оно должно вести себя, прежде чем его реализовать .
    • Это часто означает более обоснованные решения о реализации .
  • Документация
    • Набор тестов должен служить документацией спецификации (требований).
    • Использование тестов для этой цели означает, что документация и реализация всегда находятся в согласованном состоянии - изменение одного означает изменение другого. Сравните с сохранением требований и дизайна на отдельном текстовом документе.

Отдельная ответственность от реализации

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

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

Гораздо важнее записать, что объект должен:

Позвольте своим клиентам изменить свое состояние

а также

Разрешить своим клиентам запрашивать его состояние

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

Тест, такой как

(The Painter class) should store the provided colour

важно для документации части TDD.

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

Отсутствие круговой инженерии ...

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

1 Броди, Майкл Л. «Джон Милопулос: шить семена концептуального моделирования». Концептуальное моделирование: основы и приложения. Springer Berlin Heidelberg, 2009. 1-9.

... и как TDD решает это

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

Сначала проект, потом реализация

В рамках TDD мы сначала пишем неудачный приемочный тест, а затем пишем код, который позволяет им пройти.

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

Почему вы должны исключить сеттеры и геттеры?

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

Так что спросите себя:

Должен ли человек, пишущий тесты для класса, упомянуть методы получения и установки.

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

Очевидно, что если вы сначала напишите код, ответ может быть не таким четким.

Исключения

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

Например, локальный метод 'B ()':

function A() {

    // B() will be called here    

    function B() {
        ...
    }
} 

Или частная функция square()здесь:

class Something {
private:
    square() {...}
public:
    addAndSquare() {...}
    substractAndSquare() {...}
}

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


1

Столкнувшись с философским вопросом, вернитесь к требованиям вождения.

Ваша цель - выпускать достаточно надежное программное обеспечение по конкурентоспособным ценам?

Или это для производства программного обеспечения с максимально возможной надежностью практически независимо от стоимости?

До некоторой степени, две цели - качество и скорость разработки - соотносятся: вы тратите меньше времени на написание тестов, чем на исправление дефектов.

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

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

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


0

Ваш ответ на этот вопрос зависит от вашей философии (вы считаете, что это Чикаго против Лондона? Я уверен, что кто-то найдет это). Жюри до сих пор не решило вопрос о наиболее эффективном по времени подходе (потому что, в конце концов, это самый большой драйвер этого времени, затрачиваемого на исправления).

Некоторые подходы говорят, что тестируют только открытый интерфейс, другие говорят, что проверяют порядок каждого вызова функции в каждой функции. Было много священных войн. Мой совет - попробовать оба подхода. Выберите код и сделайте его как X, а другой как Y. После нескольких месяцев тестирования и интеграции вернитесь и посмотрите, какой из них лучше соответствует вашим потребностям.


0

Это сложный вопрос.

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

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

Лично я хотел бы, чтобы охват по линиям был на уровне 85-95% и проходил проверку на основной линии, чтобы гарантировать, что существующее покрытие модульных тестов на строку достигает этого уровня для всех файлов кода и чтобы не было обнаружено ни одного файла.

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


-1

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

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

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

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


-4

Это заставило меня задуматься. Должны ли мы стремиться к максимальному охвату тестами%?

Да, в идеале 100%, но некоторые вещи не тестируются на единицу.

геттеры и сеттеры (если в них нет логики)

Геттеры / сеттеры глупы - просто не используйте их. Вместо этого поместите переменную-член в публичный раздел.

"шаблонный" код

Получите общий код и протестируйте его. Это должно быть так просто.

Существуют ли какие-либо рациональные / не "легковоспламеняющиеся" причины, по которым нужно тестировать каждую (или столько, сколько он может) строку кода?

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

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


Что ж, давайте прямо скажем: я не имел в виду, что не использую тесты TDD / write. Наоборот. Я знаю, что тесты могут найти ошибку, о которой я не думал, но что тут тестировать? Я просто думаю, что такой метод является одним из «не тестируемых на единицу». Как сказал Петер Тёрёк (цитируя Кента Бека), вы должны проверить то, что может сломаться. Что здесь может сломаться? Не очень много (в этом методе есть только простое делегирование). Я МОГУ написать модульное тестирование, но оно будет просто издеваться над DAO и утверждать, а не тестировать. Что касается геттеров / сеттеров, то некоторые фреймворки требуют их.
Zenzen

1
Кроме того, так как я не заметил этого: «Извлеките общий код и протестируйте его. Это должно быть так просто». Что ты имеешь в виду? Это класс обслуживания (на уровне обслуживания между GUI и DAO), он является общим для всего приложения. Не могу сделать его более общим (так как он принимает некоторые параметры и вызывает определенный метод в DAO). Единственная причина этого заключается в том, чтобы придерживаться многоуровневой архитектуры приложения, чтобы графический интерфейс не вызывал DAO напрямую.
Zenzen

20
-1 для «Получатели / Установщики глупы - просто не используйте их. Вместо этого поместите переменную-член в публичный раздел». - Очень неправильно. Это обсуждалось несколько раз на SO . Использование открытых полей везде на самом деле даже хуже, чем использование методов получения и установки везде.
Петер Тёрёк
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.