Как вывести модульное тестирование на новый уровень

Юнит-тесты — это круто.

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

Это хороший навык — знать, как правильно тестировать собственный код — знать, что тестировать, а что пропустить; зная, сколько различных сценариев нужно проверить; зная, что должно быть изолированным тестом, а что должно быть далеко идущим интеграционным тестом — есть много разных соображений.

Одним из таких сценариев является написание ряда тестов, которые отличаются только входными данными. Это то, что мы рассмотрим здесь.

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

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

И когда мы запускаем тест, мы видим, что он отображается в нашем обозревателе тестов (если мы используем Visual Studio 2022):

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

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

Когда мы запускаем это, мы видим оба наших теста в списке:

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

Можете ли вы уловить там запах кода?

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

Решение? Параметризированные тесты.

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

В этой статье мы рассмотрим 3 основных фреймворка для тестирования (xUnit, NUnit и MSTest) и начнем с xUnit.

xUnit

Вот как мы можем обновить этот тест, чтобы он был параметризован в xUnit:

Это параметризованный тест с одним набором входных данных. Основные отличия:

  • Вместо атрибута [Fact] у нас есть атрибут [Theory].
  • У нас есть новый атрибут [InlineData] с некоторыми данными.
  • Теперь у нас есть параметры в самом методе тестирования.
  • Тест использует эти параметры вместо жесткого кодирования входных/ожидаемых выходных данных.

Теперь добавим к этому тесту остальные тестовые примеры:

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

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

Теперь, когда мы увидели, как это работает в xUnit, давайте посмотрим, как это работает в других тестовых средах.

(Спойлер: это почти то же самое)

NUnit

Если вы используете NUnit, тесты работают очень похоже. Пройдите базовый тест NUnit:

Мы можем параметризовать его следующим образом:

Обратите внимание, что атрибут [Test] был заменен отдельными атрибутами [TestCase] с входными/ожидаемыми выходы.

МСТест

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

И чтобы параметризовать его, мы имеем:

Обратите внимание, что мы заменили атрибут [TestMethod] на [DataTestMethod] и добавили каждый тестовый пример как атрибут [DataRow].

Краткое содержание

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

Мы можем сделать это с помощью xUnit, NUnit и MSTest и, возможно, любой другой полуприличной тестовой среды, которую вы можете использовать.

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

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

Эпилог

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