Новое в модульном тестировании, как писать отличные тесты? [закрыто]

267

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

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

Я уже написал тесты для нескольких классов, но теперь мне интересно, правильно ли я это делаю.

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

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

Редактировать: я хотел бы поблагодарить Stack Overflow, у меня были отличные материалы менее чем за 15 минут, которые ответили больше часов онлайн-чтения, которые я только что сделал.

pixelastic
источник
1
Это лучшая книга для модульного тестирования: manning.com/osherove В ней описываются все лучшие практики, что можно и чего нельзя делать для модульного тестирования.
Эрви Б
Все эти ответы не учитывают, что модульное тестирование похоже на документацию. Следовательно, если вы напишите функцию, вы запишите ее намерения, описав ее входы и выходы (и, возможно, побочные эффекты). Модульное тестирование предназначено для проверки этого. И если вы (или кто-то другой) позже внесете изменения в код, документы должны объяснить границы того, какие изменения могут быть внесены, а модульные тесты обеспечат сохранение этих границ.
Томас Темпельманн

Ответы:

187

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

Я думаю, что вы делаете это неправильно.

Модульный тест должен:

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

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

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

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

testAdd()
{
    int x = 5;
    int y = -2;
    int expectedResult = 3;
    Calculator calculator = new Calculator();
    int actualResult = calculator.Add(x, y);
    Assert.AreEqual(expectedResult, actualResult);
}

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

Марк Байерс
источник
13
Большое спасибо, ваш ответ был более полным. Теперь я лучше понимаю, для чего на самом деле предназначены фиктивные объекты: мне не нужно подтверждать каждый вызов других методов, только соответствующие. Мне также не нужно знать, КАК все сделано, но что они делают правильно.
пиксельный
2
Я с уважением думаю, что вы делаете это неправильно. Модульные тесты - это поток выполнения кода (тестирование белого ящика). Тестирование черного ящика (что вы предлагаете) - это обычно метод, используемый в функциональном тестировании (тестирование системы и интеграции).
Уэс
1
«Юнит-тест должен проверить один метод», на самом деле я не согласен. Модульный тест должен проверить одну логическую концепцию. Хотя это часто представляется как один метод, это не всегда так
robertmain
35

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

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

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

Все простые примеры, такие как « function square(number)отлично» и все такое, и, вероятно, являются плохими кандидатами, чтобы тратить много времени на тестирование Те, которые выполняют важную бизнес-логику, - вот где важно тестирование. Проверьте требования. Не просто проверить сантехнику. Если требования меняются, то угадайте, что, тесты тоже должны.

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

Дмитрий Лихтен
источник
2
Хороший ответ дал мне уверенность, что написание тестов после кода все еще может быть полезным и возможным.
пиксельный
2
Прекрасный недавний пример. У меня была очень простая функция. Передайте это правда, он делает одно, ложь - другое. ОЧЕНЬ ПРОСТО. Было 4 проверки, чтобы убедиться, что функция делает то, что намеревается делать. Я немного меняю поведение. Запустите тесты, POW проблема. Самое смешное, что при использовании приложения проблема не проявляется, она возникает только в сложном случае. Тестовый случай нашел его, и я избавил себя от головной боли.
Дмитрий Лихтен
«Тесты должны проверить намерение». Это, я думаю, подводит итог, что вы должны пройти по предполагаемому использованию кода и убедиться, что код может вместить их. Это также указывает на то, что тест должен на самом деле тестировать, и на идею, что, когда вы вносите изменения в код, в тот момент, когда вы можете не задумываться над тем, как это изменение влияет на все предписанные варианты использования кода - тест защищает от изменений, которые не удовлетворяют всем предполагаемым случаям использования.
Greenstick
18

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

Перемещение существующего кода в Test Driven Development

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

Дэвид
источник
3
Если вы пишете тесты первыми или вторыми, это нормально, но когда вы пишете тесты, вы гарантируете, что ваш код тестируемый, чтобы вы могли писать тесты. Вы часто задумываетесь, «как я могу это проверить», что само по себе приводит к написанию лучшего кода. Модернизация тестовых случаев - это всегда большое нет-нет. Очень сложно. Это не проблема времени, это проблема количества и тестируемости. Я не могу сейчас подойти к своему боссу и сказать, что хочу написать тестовые примеры для наших более чем тысячи таблиц и их использования, а это слишком много сейчас, заняло бы у меня год, а некоторые логические решения были забыты. Так что не откладывайте это слишком долго: P
Дмитрий Лихтен
2
Предположительно, принятый ответ изменился. Есть ответ от Линкса, который рекомендует Искусство юнит-тестирования Роя Ошерова, manning.com/osherove
thelem
15

Не пишите тесты, чтобы получить полное покрытие вашего кода. Написать тесты, которые гарантируют ваши требования. Вы можете обнаружить кодовые пути, которые не нужны. И наоборот, если они необходимы, они там, чтобы выполнить какое-то требование; найди, что это такое, и проверь требование (не путь).

Держите свои тесты маленькими: один тест за требование.

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

Джон Рейд
источник
Спасибо, имеет смысл проводить только небольшие тесты для небольших требований, по одному за раз. Урок выучен.
пиксельный
13

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

fresskoma
источник
Спасибо ! У меня было чувство, что я делаю это неправильно, но лучше, если кто-то скажет мне.
пиксельный
8

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

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

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

Джастин Нисснер
источник
Да, я бы хотел сделать это, за исключением того, что методы уже написаны. Я просто хочу проверить их. В будущем я напишу тесты перед методами.
пиксельный
2
@pixelastic притворяться, что методы не были написаны?
совершеноандройдер
4

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

When I'm writing tests for a method, I have the feeling of rewriting a second time what I          
already wrote in the method itself.
My tests just seems so tightly bound to the method (testing all codepath, expecting some    
inner methods to be called a number of times, with certain arguments), that it seems that
if I ever refactor the method, the tests will fail even if the final behavior of the   
method did not change.

Это потому, что вы пишете свои тесты после того, как написали свой код. Если бы вы сделали это наоборот (сначала написали тесты), этого бы не случилось.

hvgotcodes
источник
Спасибо за пример черного ящика, я так не думал. Хотелось бы, чтобы я обнаружил модульное тестирование раньше, но, к сожалению, это не так, и я застрял с устаревшим приложением, в которое можно добавлять тесты. Нет ли способа добавить тесты в существующий проект, не чувствуя себя разбитым?
пиксельный
1
Написание тестов после отличается от написания тестов раньше, так что вы застряли с ним. однако, что вы можете сделать, так это настроить тесты так, чтобы они сначала проваливались, а затем заставляли их проходить, подвергая ваш класс тесту… делайте что-то в этом роде, помещая свой экземпляр в тест после того, как тест первоначально провалился. То же самое и с макетами - изначально макет не имеет ожиданий и потерпит неудачу, потому что тестируемый метод будет что-то делать с макетом, а затем выполнить тест. Я не удивлюсь, если вы найдете много ошибок таким образом.
hvgotcodes
Кроме того, будьте действительно конкретными с вашими ожиданиями. Не утверждайте только, что тест возвращает объект, проверьте, что объект имеет различные значения на нем. Проверьте, что когда значение должно быть нулевым, это так. Вы также можете немного разбить его, выполнив некоторый рефакторинг, который вы собирались сделать, после добавления некоторых тестов.
hvgotcodes