Модульное тестирование
В этой статье мы намерены пользоваться встроенной поддержкой модульного тестирования, предлагаемой Visual Studio, хотя доступны и другие пакеты модульного тестирования .NET. Наиболее популярным из них является, пожалуй, NUnit, однако все пакеты тестирования в основном делают одно и то же. Причина выбора инструментов тестирования Visual Studio связана с привлекательностью интеграции с остальными частями IDE-среды.
Для работы со встроенными средствами модульного тестирования Visual Studio в пример проекта (который мы начали разрабатывать в предыдущей статье) была добавлена новая реализация интерфейса IDiscountHelper. Создайте в папке Models новый файл по имени MinimumDiscountHelper.cs с содержимым, приведенным в примере ниже:
Наша цель в этом примере — заставить класс MinimumDiscountHelper продемонстрировать следующие аспекты поведения:
если общая сумма больше $100, скидка будет составлять 10%;
если общая сумма находится в промежутке между $10 и $100 включительно, скидка будет составлять $5;
для общей суммы, не превышающей $10, скидка не предоставляется;
для отрицательной общей суммы будет сгенерировано исключение ArgumentOutOfRangeException.
Класс MinimumDiscountHelper пока еще не реализует ни одного из перечисленных аспектов поведения. Мы будем следовать , сначала написав модульные тесты и только затем реализовав код.
Создание проекта модульного тестирования
Первый шаг заключается в создании проекта модульного тестирования, для чего в окне Solution Explorer щелкните правой кнопкой мыши на элементе верхнего уровня (Решение (Solution)) и выберите в контекстном меню пункт Add —> New Project (Добавить —> Новый проект).
На необходимость создания проекта тестирования можно также указать при создании нового проекта MVC: в диалоговом окне, где выбирается начальное содержимое для проекта MVC. Для этого предусмотрен флажок Add Unit Tests (Добавить модульные тесты).
Откроется диалоговое окно Add New Project (Добавление нового проекта). В разделе шаблонов Visual C# в левой панели выберите элемент Test (Тестирование) и удостоверьтесь, что в средней панели выбран вариант Unit Test Project (Проект модульного тестирования), как показано на рисунке:

Укажите EssentialTools.Tests в качестве имени проекта и щелкните на кнопке OK, чтобы создать новый проект, который будет добавлен в текущее решение Visual Studio наряду с проектом приложения MVC.
Проекту тестирования необходимо предоставить ссылку на проект приложения, чтобы получить доступ к классам для выполнения применительно к ним тестов. В окне Solution Explorer щелкните правой кнопкой мыши на элементе References для проекта EssentialTools.Tests и выберите в контекстном меню пункт Add Reference (Добавить ссылку). Щелкните на элементе Solution (Решение) в левой панели и отметьте флажок рядом с EssentialTools:

Создание модульных тестов
Модульные тесты будут добавляться в файл UnitTest1.cs внутри проекта EssentialTools.Tests. Платные редакции Visual Studio обладают удобными средствами автоматической генерации тестовых методов для класса, которые в редакции Express не доступны, однако создавать полезные и значащие тесты можно и вручную.
Для начала внесите изменения, показанные в примере ниже:
Здесь был добавлен одиночный модульный тест. Класс, который содержит тесты, аннотирован атрибутом TestClass, а отдельные тесты представляют собой методы, аннотированные атрибутом TestMethod. Не все методы в тестовом классе должны быть модульными тестами. Для демонстрации сказанного мы определили метод getTestObject(), который будет использоваться для организации тестов. Поскольку этот метод не имеет атрибута TestMethod, среда Visual Studio не будет трактовать его как модульный тест.
Обратите внимание на добавление оператора using для импорта пространства имен EssentialTools.Models в тестовый класс. Тестовые классы — это всего лишь обычные классы C#, которые совершенно не осведомлены о проекте MVC. Всю «магию» тестирования в проекте обеспечивают атрибуты TestClass и TestMethod.
Как видите, при создании этого метода модульного теста мы следовали шаблону , который был описан в статье «Автоматизированное тестирование».
Существует огромное количество соглашений по именованию модульных тестов, но мы придерживаемся принципа назначения таких имен, которые ясно отражают то, что проверяется тестом. Наш метод модульного теста называется Discount_Above_100() (скидка на сумму выше $100) и выглядит для нас четко и ясно. Тем не менее, в действительности важным является лишь то, чтобы вы (и ваша команда) понимали принятый шаблон именования, поэтому можете выбрать другой подход, если данный чем-то не устраивает.
В начале тестового метода мы вызываем метод getTestObject(), который создает экземпляр объекта, предназначенного для тестирования — в данном случае это MinimumDiscountHelper. Кроме того, мы определяем значение total, для которого будет проводиться тестирование. Это раздел организации модульного теста.
В разделе действия теста мы вызываем метод MinimumDiscountHelper.ApplyDiscount() и присваиваем возвращаемый им результат переменной discountedTotal. Наконец, в разделе утверждения теста мы применяем метод Assert.AreEqual() для проверки того, что значение, полученное из метода ApplyDiscount(), составляет 90% общей суммы, указанной в начале.
В классе Assert определен набор статических методов, которые можно использовать в тестах. Этот класс находится в пространстве имен Microsoft.VisualStudio.TestTools.UnitTesting вместе с рядом дополнительных классов, полезных для настройки и выполнения тестов.
Класс Assert является одним из самых часто применяемых, поэтому его важные методы кратко описаны в таблице ниже:
Утверждает, что два объекта типа T имеют одно и то же значение
Утверждает, что два объекта типа T не имеют одно и то же значение
Утверждает, что две переменные ссылаются на один и тот же объект
Утверждает, что две переменные ссылаются на разные объекты
Отрицательный результат утверждения — никакие условия не проверены
Показывает, что результат модульного теста не может быть однозначно установлен
Утверждает, что булевское значение равно true — чаще всего используется для оценки выражения, возвращающего булевский результат
Утверждает, что булевское значение равно false
Утверждает, что переменная не присвоена объектной ссылке
Утверждает, что переменная присвоена объектной ссылке
Утверждает, что объект относится к указанному типу или является производным от указанного типа
Утверждает, что объект не относится к указанному типу
Каждый статический метод в классе Assert позволяет проверить какой-то аспект модульного теста, и если проверка не проходит, эти методы генерируют исключение. Чтобы модульный тест прошел, все утверждения должны завершиться успешно.
Каждый метод из таблицы имеет перегруженную версию, которая принимает параметр string. В случае отрицательного результата утверждения эта строка помещается в элемент сообщения внутри объекта исключения. Методы AreEqual и AreNotEqual имеют несколько перегруженных версий, предназначенных для сравнения специфических типов. Например, существует версия, которая позволяет сравнивать строки без учета регистра символов.
Одним заслуживающим внимания членом пространства имен Microsoft.VisualStudio.TestTools.UnitTesting является атрибут ExpectedException. Это утверждение, которое дает положительный результат, только если модульный тест генерирует исключение с типом, указанным в параметре ExceptionType. Данный атрибут служит надежным способом обеспечения генерации исключений без необходимости в наличии блоков try. catch внутри кода модульного теста.
Теперь, когда было показано, как создать один модульный тест, к тестовому проекту можно добавить дальнейшие тесты, предназначенные для проверки других аспектов поведения класса MinimumDiscountHelper. Все добавления показаны в примере ниже, но эти модульные тесты настолько кратки и просты (в целом это является характерной особенностью модульных тестов), что подробные объяснения для них приводиться не будут:
Прохождение (и не прохождение) модульных тестов
Среда Visual Studio предоставляет окно Test Explorer (Проводник тестов), предназначенное для управления и выполнения тестов. Выберите пункт Windows —> Test Explorer в меню Test среды Visual Studio, чтобы открыть это окно, и щелкните на кнопке Run All (Запустить все) в верхнем левом углу. Вы увидите результаты, похожие на показанные на рисунке ниже:

В левой панели окна Test Explorer отображается список всех ранее определенных тестов. Разумеется, все эти тесты не прошли, поскольку тестируемый метод пока еще не реализован. Щелкнув на любом тесте в этом окне, в правой панели можно просмотреть подробную информацию о нем. Окно Test Explorer предоставляет набор разных способов для выделения и фильтрации модульных тестов и для выбора запускаемых тестов. Тем не менее, в нашем простом примере проекта мы просто запустим все тесты, щелкнув на кнопке Run All.
Реализация функциональной возможности
Теперь, когда известно, что по завершении написания кода можно будет проверить его работу, мы можем приступить к реализации необходимой функциональной возможности. При всей выполненной подготовке реализация класса MinimumDiscountHelper оказывается достаточно простой и показана в примере ниже:
Тестирование и исправление кода
Мы преднамеренно оставили ошибку в этом коде, чтобы продемонстрировать работу интерактивного модульного тестирования в Visual Studio, поэтому результат ошибки можно просмотреть, щелкнув на кнопке Run All в окне Test Explorer. Результаты тестирования показаны на рисунке ниже:

Среда Visual Studio всегда пытается переносить наиболее полезную информацию в верхнюю часть окна Test Explorer. В данной ситуации это означает, что тесты, которые не прошли, отображаются перед прошедшими тестами.
На рисунке видно, что три модульных теста прошли, но имеется проблема, обнаруженная тестовым методом Discount_Between_10_And_100. Щелкнув на этом тесте можно выяснить, что тест ожидал получить результат 5, тогда как в действительности было получено значение 10.
В этот момент мы возвращаемся к коду и видим, что ожидаемое поведение не было реализовано должным образом — в частности, скидки для общих сумм, равных 10 или 100, обрабатываются некорректно. Проблема кроется в следующем операторе из класса MinimumDiscountHelper:
Спецификация, с которой мы имеем дело, устанавливает поведение для значений из промежутка между $10 и $100 включительно, но наша реализация исключает эти значения и проверяет только величины, которые больше $10, не учитывая общую сумму, в точности равную $10. Решение выглядит довольно просто и показано в примере ниже — для изменения результата действия оператора if понадобится добавить всего один символ:
После щелчка на кнопке Run All в окне Test Explorer результаты показывают, что проблема устранена, и все тесты на коде проходят успешно:
Assert areequal c что это
Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core
Возьмем тот же проект из прошлый темы (либо создадим новый) и добавим в главный проект веб-приложения в папку Contollers новый контроллер StoreController:
Контроллер имеет только один метод, который устанавливает свойство ViewBag.Message и генерирует объект ActionResult. А также добавим для метода Index представление.
Теперь перейдем к проекту тестов и добавим в него новый класс тестов. Для этого мы можем добавить либо стандартный класс, либо использовать специальный шаблон файлов. Для этого в проекте тестов нажмем правой кнопкой мыши на каталог Controllers и в появившемся контекстном меню выберем Add->Unit Test. :

По умолчанию добавляет класс UnitTest1. Во-первых, изменим название класса и файла на StoreControllerTest . Затем изменим следующим образом сам класс:
Метод IndexViewResultNotNull тестирует результат метода — возвращаемый объект ViewResult не должен иметь значение null. Метод IndexViewEqualIndexCshtml проверяет название вызываемого представления с помощью вызова Assert.AreEqual . А метод IndexStringInViewbag проверяет значение строки в свойстве ViewBag.Message.
Хотя у нас только один метод в контроллере, для него мы создали три тестовых метода для теста каждого отдельного тестового действия. Подобная изоляция облегчает тестирования отдельных участков кода.
Перед запуском тестов перестроим главный проект. И запустим тесты. В этом случае мы увидим, что один тест не пройден — тот, который верифицирует представление:

Тест не пройден, потому что при вызове метода View нам надо явным образом указывать представление. Поэтому изменим метод Index в главном проекте следующим образом:
Снова запустим тесты. И теперь уже все тесты должны быть успешно пройдены.
Все три действия имеют одну и ту же секцию Arrange, и, возможно, было бы неплохо сразу установить все начальные настройки для всех методов. Для этого изменим в тестовом проекте класс StoreControllerTest следующим образом:
Атрибут TestInitialize позволяет задать метод, который выполняет начальную инициализацию для каждого отдельного теста. Благодаря этому код сокращен, а в тестовых методах оставлены только части Assert. Однако подобный подход надо принимать с осторожностью, так как он осложняет возможности по изменению кода. В данном случае общий контекст очень прост, но если при изменении методов будет изменяться и их контекст, то придется вносить большие изменения во всех классах тестов, а не только в отдельный метод для тестов.
Класс Assert и тестирование результата
Класс Assert из пространства имен Microsoft.VisualStudio.TestTools.UnitTesting с помощью своих статических методов позволяет верифицировать результат выполнения некоторого действия. Ранее уже было рассмотрено несколько методов, в частности, метод Assert.IsNotNull() , проверяющий, не равен ли некоторый объект значению null. Кроме того, при тестировании нам доступен еще ряд методов:
AreEqual(object expected, object actual) : проверяет, равны ли оба объекта. Имеет различные перегруженные версии, позволяющие сравнивать различные типы объектов
AreEqual<T>(T expected, T actual) : обобщенная версия предыдущего метода. Например, Assert.AreEqual<string>(«Index», result.MasterName)
AreNotEqual(object expected, object actual) : проверяет, не равны ли оба объекта. Тест проходит успешно, если объекты не равны
AreNotEqual<T>(T expected, T actual) : обобщенная версия предыдущего метода
AreSame(object expected, object actual) : проверяет, указывают ли оба объекта на один и тот же объект в памяти
AreNotSame(object expected, object actual) : проверяет, указывают ли оба объекта на разные объекты в памяти. Если они указывают на один и тот же объект, то тест заканчивается неудачно
Equals(object objA, object objB) : проверяет на равенство оба объекта
IsFalse(bool condition) : проверяет, равно ли условие condition значению false
IsTrue(bool condition) : проверяет, равно ли условие condition значению true
IsNull(object value) : проверяет, имеет ли объект value значение null
IsInstanceOfType(object value, Type expectedType) : проверяет, представляет ли объект value тип expectedType
Используя эти методы, мы можем проверить различные ситуации в своем приложении.
Assert-сообщения в тестах
В этом посте мы поговорим о том, должны ли вы использовать Assert-сообщения в ваших тестах.
Я получил интересный вопрос от коллеги читателя, на котором хотел бы остановиться поподробнее:
У меня вопрос по поводу Assert-сообщений: следует ли использовать перегрузку, содержащую параметр сообщения, и использовать ее для передачи строки, описывающей причину неудачи Assert (также “Утверждения”)?
Ответ на этот вопрос сводится к двум аспектам:
- Читаемость теста — насколько легко понять, что делает тест.
- Простота диагностики — насколько легко понять, почему тест не пройден.
Читаемость теста
Люди часто используют Assert-сообщения, чтобы помочь членам команды и самим себе в будущем понять, что происходит в тесте. Давайте рассмотрим следующий пример:
Вместо голого Assert вы также можете указать причину, по которой тестовый Assert что-либо валидирует:
Такие утверждения помогают, но они имеют свою цену. Эти сообщения требуют от вас
- Потратить время на их написание
- Поддерживать их продвижение вперед
Вводите assert-сообщения только тогда, когда это абсолютно необходимо — когда вы не можете улучшить читаемость теста каким-либо другим способом. Но даже тогда, склоняйтесь к выбору не писать их.
Самый простой способ получить быстрый выигрыш в читаемости теста — это переключиться на удобочитаемую запись утверждений. Например, NUnit имеет специальную модель утверждений на основе ограничений (constraint), которая помогает вам писать свои утверждения следующим образом:
Или вы можете использовать мои любимые Fluent Assertions:
Такие утверждения читаются на простом английском языке — именно так, как вы хотели бы, чтобы читался весь ваш код. Мы, люди, предпочитаем воспринимать информацию в форме историй. Все истории придерживаются данной модели:
Здесь Боб — субъект, открыл — действие, а дверь — объект. То же самое относится и к коду.
читает лучше чем
именно потому, что здесь прослеживается история.
Простота диагностики
Другой взгляд на assert-сообщения — с точки зрения простоты диагностики. Другими словами, простота понимания причины провала теста без изучения кода этого теста. Это полезно при чтении результатов сборки CI.
С точки зрения диагностики, следуйте данному руководству: если вы можете легко повторно запустить тест локально, этот тест не нуждается в assert-сообщении. Это верно для всех модульных тестов (поскольку они не работают с внепроцессными зависимостями), но в меньшей степени для интеграционных и сквозных тестов.
Пирамида тестирования
По мере того, как вы будете подниматься выше в тестовой пирамиде, вам может понадобиться более подробная информация, потому что интеграционные (и особенно сквозные) тесты медленнее и возможно вы не сможете их повторно запускать по желанию.
Но даже с интеграцией и сквозными тестами существуют способы облегчить диагностику, не прибегая к assert-сообщениям:
- Сделайте так, чтобы тест проверял один модуль поведения — когда тест проверяет что-то одно, часто легко определить, что пошло не так. (Это не всегда применимо к сквозным тестам, так как вы можете захотеть, чтобы такие тесты проверяли, как несколько единиц поведения работают вплотную). — Идеальное имя теста описывает поведение приложения в бизнес-терминах, так что даже непрограммист может его понять.
Например, ошибка в следующем утверждении:
выдает следующее сообщение об ошибке:
Комбинация таких генерируемых средой сообщений и понятных человеку имен тестов делает 90% пользовательских assert-сообщений бесполезными даже с точки зрения простоты диагностики. Единственное исключение — это длительные сквозные тесты. Они часто содержат многоэтапные проверки, поэтому имеет смысл использовать дополнительные assert-сообщения, чтобы понять, какой из шагов не удался. Однако таких сквозных тестов не должно быть много.
Конечно, чтобы воспользоваться сгенерированными фреймворком сообщениями о сбоях, вам нужно избегать общих булевых сравнений, таких как:
Потому что они приводят к следующему сообщению об ошибке:
Что не помогает с диагностикой вообще.
Резюме
Вы часто ощущаете лень, когда речь идет о написании утверждений? Что ж, теперь вы можете оправдать ее и отослать своих коллег к этой статье.
«Я рад, что у этого есть название»
MSTest Assert class — an overview
You know, Unit Tests are our friends. Usually skipped, often misused, but they are still with us.
With or without mocks, they can help us while creating an application, ensuring that the methods we create do what they are supposed to do.
In this kind of series, I’m going to dive into basic concepts of the MS Test Framework. I’ll show you some important concepts often ignored.
In this article, I’ll analyse the Assert class and some of the methods that are ignored or misunderstood.
In the second article of this series, I’ll explain another useful class that only a few people use in their test: the StringAssert class.
Another class to keep in mind CollectionAssert, that will be the topic of the third part of this series -spoiler alert: it is about collections!
Let’s have a go!
WAIT A MINUTE! This will be a loooong post, but don’t panic, that’s just because there are lots of examples!
Basic concepts
With Visual Studio we can create Unit Tests for our projects. Here’s a simple example:
This is a terribly, terribly, dumb test: it checks if true is true.
As you can see, the Assert class contains static methods, and it says if the test will pass or will fail.
Note 1: the Assert class is not native of C#: its namespace is Microsoft.VisualStudio.TestTools.UnitTesting .
Note 2: you cannot create sub-classes since this class is sealed.
This class provides the most general checks, those based on equality and general assertions. You can find the documentation at this page.
For almost every method I’ll show in this article there is a specular method that checks if the condition is not verified. For Assert.IsTrue there is Assert.IsFalse, for Assert.AreEqual there is Assert.AreNotEqual and so on. The only exception here is the ThrowsException method.
Every method has two overrides that allow you to add an error message as a string and to provide custom parameters to pass to the string, in order to format it as you do with String.Format() .
Assert.IsTrue
With these methods, you can check if a generic condition is true or false.
Assert.AreEqual
This methods checks if the two parameters have the same value or not.
There are lots of overloads for this method, depending on the type of parameters under the microscope.
For each type passed as parameter there are different parameters.
Assert.AreEqual with Int values
You can check if two Int are equals. But you can use also Int16, Int32 and Int64 to be compared:
Assert.AreEqual with Single and Double values
Since the values are always rounded in a way that depends on the inner representation of the Single and Double data type, you must specify a third value for the comparison: the delta.
So, the test will fail if the actual value differs more than delta from the expected value.
Assert.AreEqual with String values
This overload was made for the simple comparison of strings.
Case sensitivity
With a Boolean flag you can specify whether the comparison must ignore case.
CultureInfo
Sometimes you need to check if two strings are equals according to a specific culture. Well, you can add a CultureInfo parameter to the method to achieve the result.
You might think “Do I really need to check for the culture?”. Usually not, unless you are Turkish.
The Turkish i problem
Have you ever heard of the Turkish I problem? In short, for the Turkish alphabet the uppercase i is not I, but İ. You can see a more detailed article here.
So when comparing strings you should keep this problem in mind.
Assert.AreEqual with Objects
With objects things get a bit more complicated. Let’s say we have this class:
Now have a look at this test:
Will the test pass? The answer is… NO! Why?
Well, the two objects look identical, and have the same values for every field. But they refer to different memory location. As you know, equality on objects is made on the object reference — here a really good article.
So… How can we pass the test?
Override Equals
The solution is to override the Equals() method of the Object class. This will let you specify a custom way to compare two objects without comparing the object reference.
First of all I've created a new class, UpdatedUser, that is similar to the User class seen before but with an override of the Equals method.
Now we can play with this new class.
Assert.AreEqual with Structs
And what about structs? Oh, come on, who uses structs?? Well, who am I to judge you? 🙂
Ok, seriously. Structs are just like value types like int, but they can add additional fields.
So the equality check is the simplest you can imagine.
Assert.AreSame
This method checks if the references of the two values are the same.
As you can see, a and b are exactly the same struct, so the override of the Equals method is not necessary. With this method you can verify by yourself that when adding an element in a List you are adding a reference to an object, not cloning that one:
Assert.IsInstanceOfType
Well, you can imagine what this method does… In the examples below I’ll show you also the IsNotInstanceOfType method, just to have a countercheck on what is inheritance. In fact, in this example I created the AdminUser class that extends the User class seen before.
Assert.IsNull
It’s not difficult to guess what this method checks…
Assert.ThrowsException
Until now we assumed that all our methods return a value and that we should just check if that value is correct. But some times methods throw exceptions, and we have to handle them.
That’s why this method comes handy.
Suppose you have a simple method like this one:
We know that the method won’t fail if you pass a valid username. But we also want to ensure that with a specific condition it will throw an exception. And we can check it this way:
Perfect! Or not?
What if the exception thrown is not of the same type of the one expected?
Let’s modify the IsAuthorized method.
This way we are giving more information on why the method fails. But the test seen before will fail because that’s not the exception expected (we are expecting an Exception but we receive an ArgumentNullException).
Is there a way to create generic tests?
Well… no.
Just look at what happens into the ThrowsException method and find out why.
Wrapping Up
This was a long article, I know. But here I have listed a few methods I don’t see used as much as they should. As I said before, nearly every method has its negative counterpart, so you have a rich set of checks to use.
In the next article we’ll have a look at the StringAssert class, that’s -obviously- specific for strings.