Я пытаюсь сделать мой код более надежным, и я читал о модульном тестировании, но мне очень трудно найти реальное полезное применение. Например, пример из Википедии :
public class TestAdder {
public void testSum() {
Adder adder = new AdderImpl();
assert(adder.add(1, 1) == 2);
assert(adder.add(1, 2) == 3);
assert(adder.add(2, 2) == 4);
assert(adder.add(0, 0) == 0);
assert(adder.add(-1, -2) == -3);
assert(adder.add(-1, 1) == 0);
assert(adder.add(1234, 988) == 2222);
}
}
Я чувствую, что этот тест совершенно бесполезен, потому что вы должны вручную вычислить желаемый результат и протестировать его, я чувствую, что лучший тестовый модуль здесь будет
assert(adder.add(a, b) == (a+b));
но тогда это просто кодирование самой функции в тесте. Может кто-нибудь дать мне пример, где юнит-тестирование действительно полезно? К вашему сведению, я в настоящее время кодирую в основном «процедурные» функции, которые принимают ~ 10 логических значений и несколько целочисленных значений и дают мне результат int, основанный на этом, я чувствую, что единственное модульное тестирование, которое я мог бы сделать, это просто перекодировать алгоритм в тест. редактировать: я также должен был уточнить, это при переносе (возможно, плохо разработан) рубиновый код (который я не сделал)
источник
How does unit testing work?
Никто на самом деле не знает :)Ответы:
Модульные тесты, если вы тестируете достаточно маленькие блоки, всегда утверждают, что они очевидны.
Причина, по которой
add(x, y)
даже упоминается о модульном тесте, заключается в том, что через некоторое время кто-нибудь зайдетadd
и поместит специальный код обработки налоговой логики, не понимая, что add используется везде.Юнит - тесты являются очень много об ассоциативном принципе: если А делает B, и B делает С, то А делает C. «А делает C» тест на более высоком уровне. Например, рассмотрим следующий, полностью допустимый бизнес-код:
На первый взгляд это выглядит как отличный метод для юнит-тестирования, потому что он имеет очень четкую цель. Тем не менее, он делает около 5 разных вещей. Каждая вещь имеет действительный и недействительный регистр, и будет делать огромную перестановку юнит-тестов. В идеале это разбивается дальше:
Теперь мы до единиц. Один модульный тест не заботится о том, что
_userRepo
считается правильным поведениемFetchValidUser
, только о том, что он называется. Вы можете использовать другой тест, чтобы точно определить, что представляет собой действительный пользователь. Аналогично дляCheckUserForRole
... вы отделили свой тест от знания, как выглядит структура ролей. Вы также полностью отделили всю свою программу от привязкиSession
. Я полагаю, что все недостающие части будут выглядеть так:Рефакторинг вы выполнили несколько вещей одновременно. Программа гораздо более полезна для удаления базовых структур (вы можете отказаться от слоя базы данных для NoSQL) или для плавного добавления блокировок, как только вы поймете,
Session
что они не ориентированы на многопоточность или что-то в этом роде. Вы также дали очень простые тесты для написания этих трех зависимостей.Надеюсь это поможет :)
источник
Я совершенно уверен, что каждая из ваших процедурных функций является детерминированной, поэтому она возвращает конкретный результат int для каждого заданного набора входных значений. В идеале у вас должна быть функциональная спецификация, из которой вы можете определить, какой результат вы должны получить для определенных наборов входных значений. В отсутствие этого вы можете запустить код ruby (который, как предполагается, работает правильно) для определенных наборов входных значений, и записать результаты. Затем вам нужно жестко кодировать результаты в вашем тесте. Предполагается, что тест является доказательством того, что ваш код действительно дает результаты, которые, как известно, являются правильными .
источник
Поскольку никто, кажется, не предоставил фактический пример:
Ты говоришь:
Но дело в том, что вы гораздо реже ошибаетесь (или, по крайней мере, чаще замечаете свои ошибки) при выполнении ручных вычислений, чем при кодировании.
Какова вероятность того, что вы допустили ошибку в своем десятичном коде? Довольно вероятно. Насколько вероятно, что вы допустили ошибку при ручном преобразовании десятичных чисел в римские? Не очень вероятно. Вот почему мы тестируем по ручным расчетам.
Какова вероятность того, что вы допустите ошибку при реализации функции квадратного корня? Довольно вероятно. Насколько вероятно, что вы допустили ошибку при расчете квадратного корня вручную? Вероятно, более вероятно. Но с sqrt вы можете использовать калькулятор, чтобы получить ответы.
Поэтому я собираюсь порассуждать о том, что здесь происходит. Ваши функции довольно сложны, поэтому из входных данных сложно определить, какими должны быть выходные данные. Чтобы сделать это, вы должны вручную выполнить (в своей голове) функцию, чтобы выяснить, что является выходом. Понятно, что это кажется бесполезным и подверженным ошибкам.
Ключ в том, что вы хотите найти правильные результаты. Но вы должны проверить эти результаты в отношении чего-то, что, как известно, является правильным. Нет смысла писать свой собственный алгоритм для вычисления этого, потому что это может быть неправильно. В этом случае слишком сложно вручную вычислить значения.
Я вернусь к коду ruby и выполню эти оригинальные функции с различными параметрами. Я бы взял результаты кода ruby и поместил их в модульный тест. Таким образом, вам не нужно делать ручной расчет. Но вы тестируете против исходного кода. Это должно помочь сохранить результаты одинаковыми, но если в оригинале есть ошибки, это не поможет. По сути, вы можете рассматривать исходный код как калькулятор в примере sqrt.
Если вы показали реальный код, который вы переносите, мы могли бы предоставить более подробный отзыв о том, как решить проблему.
источник
Вы почти правы для такого простого класса.
Попробуйте это для более сложного калькулятора .. Как калькулятор счета в боулинге.
Ценность модульных тестов становится легче увидеть, когда у вас есть более сложные «бизнес» правила с различными сценариями для тестирования.
Я не говорю, что вы не должны проверять прогон обычного калькулятора (Выдает ли ваш счет калькулятора значения, такие как 1/3, которые не могут быть представлены? Что это делает с делением на ноль?), Но вы увидите цените более четко, если вы тестируете что-то с большим количеством веток, чтобы охватить
источник
Несмотря на религиозное фанатизм о 100% охвате кода, я скажу, что не каждый метод должен подвергаться модульному тестированию. Только функциональность, которая содержит значимую бизнес-логику. Функция, которая просто добавляет число, проверять бессмысленно.
Здесь есть ваша настоящая проблема. Если модульное тестирование кажется неестественно сложным или бессмысленным, то это, вероятно, из-за недостатка дизайна. Если бы он был более объектно-ориентированным, ваши сигнатуры методов не были бы такими массивными, и было бы меньше возможных входных данных для тестирования.
Мне не нужно идти в мой ОО превосходит процедурное программирование ...
источник
На мой взгляд, юнит-тесты даже полезны для вашего маленького класса сумматоров: не думайте о «перекодировании» алгоритма и думайте о нем как о чёрном ящике, единственное знание о котором вы знаете - это функциональное поведение (если вы знакомы) с быстрым умножением вы знаете несколько более быстрых, но более сложных попыток, чем использование "a * b") и вашего открытого интерфейса. Чем вы должны спросить себя "Что, черт возьми, может пойти не так?" ...
В большинстве случаев это происходит на границе (я вижу, вы тестируете уже добавление этих шаблонов ++, -, + -, 00 - время, чтобы завершить их к - +, 0+, 0-, +0, -0). Подумайте о том, что происходит в MAX_INT и MIN_INT при сложении или вычитании (добавлении негативов;)). Или постарайтесь убедиться, что ваши тесты выглядят совершенно точно так, как это происходит в районе нуля.
В целом секрет очень прост (может быть, и для более сложных;)) для простых классов: подумайте о контрактах вашего класса (см. Дизайн по контракту), а затем протестируйте их. Чем лучше вы будете знать свои инв, пре и пост, тем «полнее» ваши тесты.
Подсказка для ваших тестовых классов: попробуйте написать только одно утверждение в методе. Дайте методам хорошие имена (например, «testAddingToMaxInt», «testAddingTwoNegatives»), чтобы получить наилучшую обратную связь, если ваш тест не пройден после изменения кода.
источник
Вместо того чтобы проверять возвращаемое вручную возвращаемое значение или дублировать логику в тесте для вычисления ожидаемого возвращаемого значения, протестируйте возвращаемое значение для ожидаемого свойства.
Например, если вы хотите протестировать метод, который инвертирует матрицу, вы не хотите вручную инвертировать входное значение, вы должны умножить возвращаемое значение на вход и проверить, что вы получаете матрицу тождественности.
Чтобы применить этот подход к вашему методу, вам нужно будет рассмотреть его назначение и семантику, чтобы определить, какие свойства будет иметь возвращаемое значение относительно входных данных.
источник
Модульные тесты являются инструментом производительности. Вы получаете запрос на изменение, реализуете его, затем запускаете свой код через гамбит модульного тестирования. Это автоматическое тестирование экономит время.
I feel that this test is totally useless, because you are required to manually compute the wanted result and test it, I feel like a better unit test here would be
Спорный вопрос. Тест в этом примере просто показывает, как создать экземпляр класса и выполнить его через серию тестов. Концентрируясь на мелочах одной реализации, не хватает леса для деревьев.
Can someone provide me with an example where unit testing is actually useful?
У вас есть сотрудник Employee. Сущность содержит имя и адрес. Клиент решает добавить поле ReportsTo.
Это базовый тест BL для работы с сотрудником. Код пропустит / провалит изменение схемы, которое вы только что сделали. Помните, что утверждения - не единственное, что делает тест. Выполнение кода также гарантирует, что никакие исключения не всплывают.
Со временем наличие тестов облегчает внесение изменений в целом. Код автоматически проверяется на наличие исключений и ваших утверждений. Это позволяет избежать значительных накладных расходов, возникающих при ручном тестировании группой обеспечения качества. Хотя пользовательский интерфейс все еще довольно сложен для автоматизации, остальные слои, как правило, очень просты, если вы правильно используете модификаторы доступа.
I feel like the only unit testing I could do would be to simply re-code the algorithm in the test.
Даже процедурная логика легко инкапсулируется внутри функции. Инкапсулируйте, создайте экземпляр и передайте int / примитив для тестирования (или фиктивный объект). Не копируйте вставьте код в модульный тест. Это побеждает СУХОЙ. Он также полностью игнорирует тест, потому что вы тестируете не код, а копию кода. Если код, который должен был быть проверен, изменяется, тест все равно проходит!
источник
Принимая ваш пример (с небольшим рефакторингом),
не помогает:
math.add
ведет себя внутренне,Это почти как сказать:
math.add
могут содержать сотни LOC; см. Ниже).Это также означает, что вам не нужно добавлять тесты, такие как:
Они не помогают ни, по крайней мере, после того, как вы сделали первое утверждение, второе не принесло ничего полезного.
Вместо этого, как насчет:
Это самоочевидно и чертовски полезно как для вас, так и для человека, который будет поддерживать исходный код позже.Представьте, что этот человек делает небольшую модификацию для
math.add
упрощения кода и оптимизации производительности, и видит результат теста следующим образом:этот человек сразу поймет, что вновь измененный метод зависит от порядка аргументов: если первый аргумент является целым числом, а второй - длинным числом, результатом будет целое число, в то время как ожидалось длинное число.
Точно так же получение действительного значения
4.141592
при первом утверждении не требует пояснений: вы знаете, что метод должен работать с большой точностью , но на самом деле он терпит неудачу.По той же причине два следующих утверждения могут иметь смысл в некоторых языках:
Кроме того, как насчет:
Это говорит само за себя: вы хотите, чтобы ваш метод мог правильно справляться с бесконечностью. Собирается за пределы бесконечности или создание исключения не является ожидаемым поведением.
Или, может быть, в зависимости от вашего языка, это будет иметь больше смысла?
источник
Для очень простой функции, такой как add, тестирование может считаться ненужным, но по мере усложнения ваших функций становится все более и более очевидным, почему тестирование необходимо.
Подумайте о том, что вы делаете, когда программируете (без юнит-тестирования). Обычно вы пишете некоторый код, запускаете его, видите, что он работает, и переходите к следующему, верно? По мере того, как вы пишете больше кода, особенно в очень больших системах / графических интерфейсах / веб-сайтах, вы обнаруживаете, что вам нужно все больше и больше «запускать и проверять, работает ли он». Вы должны попробовать это и попробовать это. Затем вы вносите несколько изменений, и вам приходится пробовать одни и те же вещи снова и снова. Становится очень очевидным, что вы могли бы сэкономить время, написав модульные тесты, которые бы автоматизировали всю часть «запуска и проверки работоспособности».
По мере того, как ваши проекты становятся все больше и больше, количество вещей, которые вам нужно «запустить и посмотреть, работает ли», становится нереальным. Таким образом, вы в итоге просто запускаете и пробуете несколько основных компонентов GUI / проекта, а затем надеетесь, что все остальное в порядке. Это рецепт катастрофы. Конечно, вы, как человек, не можете неоднократно проверять каждую возможную ситуацию, которую могут использовать ваши клиенты, если буквально сотни людей используют GUI. Если у вас есть модульные тесты, вы можете просто запустить тест перед выпуском стабильной версии или даже перед фиксацией в центральном хранилище (если оно используется на вашем рабочем месте). И, если позже будут обнаружены какие-либо ошибки, вы можете просто добавить модульный тест, чтобы проверить его в будущем.
источник
Одним из преимуществ написания модульных тестов является то, что он помогает вам писать более надежный код, заставляя задуматься о крайних случаях. Как насчет тестирования для некоторых крайних случаев, таких как целочисленное переполнение, десятичное усечение или обработка нулевых значений для параметров?
источник
Возможно, вы предполагаете, что add () был реализован с помощью инструкции ADD. Если какой-то младший программист или инженер по аппаратному обеспечению повторно реализовал функцию add (), используя ANDS / ORS / XORS, инвертирование битов и сдвиги, вы можете захотеть протестировать его по команде ADD.
В общем, если вы замените кишки add () или тестируемого модуля на генератор случайных чисел или выходных данных, как вы узнаете, что что-то сломалось? Закодируйте эти знания в своих модульных тестах. Если никто не может сказать, сломался ли он, просто зарегистрируйте код для rand () и идите домой, ваша работа выполнена.
источник
Возможно, я пропустил его среди всех ответов, но для меня главное, что стоит за модульным тестированием, заключается не столько в доказательстве правильности метода сегодня, сколько в том, что он доказывает постоянную правильность этого метода, когда [когда-либо] вы его меняете .
Возьмите простую функцию, например, вернуть количество элементов в некоторой коллекции. Сегодня, когда ваш список основан на одной внутренней структуре данных, которую вы хорошо знаете, вы можете подумать, что этот метод настолько болезненно очевиден, что вам не нужен тест для него. Затем, через несколько месяцев или лет, вы (или кто-то еще ) решите заменить внутреннюю структуру списка. Вам все еще нужно знать, что getCount () возвращает правильное значение.
Вот где ваши юнит-тесты действительно вступают в свои права.
Вы можете изменить внутреннюю реализацию своего кода, но для любого потребителя этого кода результат останется прежним.
источник