Модульное тестирование

81

В этой статье мы намерены пользоваться встроенной поддержкой модульного тестирования, предлагаемой Visual Studio, хотя доступны и другие пакеты модульного тестирования .NET. Наиболее популярным из них является, пожалуй, NUnit, однако все пакеты тестирования в основном делают одно и то же. Причина выбора инструментов тестирования Visual Studio связана с привлекательностью интеграции с остальными частями IDE-среды.

Для работы со встроенными средствами модульного тестирования Visual Studio в пример проекта (который мы начали разрабатывать в предыдущей статье) была добавлена новая реализация интерфейса IDiscountHelper. Создайте в папке Models новый файл по имени MinimumDiscountHelper.cs с содержимым, приведенным в примере ниже:

using System;

namespace EssentialTools.Models
{
    public class MinimumDiscountHelper : IDiscountHelper
    {
        public decimal ApplyDiscount(decimal totalParam)
        {
            throw new Exception();
        }
    }
}

Наша цель в этом примере - заставить класс MinimumDiscountHelper продемонстрировать следующие аспекты поведения:

Класс MinimumDiscountHelper пока еще не реализует ни одного из перечисленных аспектов поведения. Мы будем следовать подходу разработки через тестирование (Test Driven Development - TDD), сначала написав модульные тесты и только затем реализовав код.

Создание проекта модульного тестирования

Первый шаг заключается в создании проекта модульного тестирования, для чего в окне 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 не доступны, однако создавать полезные и значащие тесты можно и вручную.

Для начала внесите изменения, показанные в примере ниже:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EssentialTools.Models;

namespace EssentialTools.Tests
{
    [TestClass]
    public class UnitTest1
    {
        private IDiscountHelper getTestObject()
        {
            return new MinimumDiscountHelper();
        }

        [TestMethod]
        public void Discount_Above_100()
        {
            // arrange (организация)
            IDiscountHelper target = getTestObject();
            decimal total = 200;

            // act (акт)
            var discountedTotal = target.ApplyDiscount(total);

            // assert (утверждение)
            Assert.AreEqual(total * 0.9M, discountedTotal);
        }
    }
}

Здесь был добавлен одиночный модульный тест. Класс, который содержит тесты, аннотирован атрибутом TestClass, а отдельные тесты представляют собой методы, аннотированные атрибутом TestMethod. Не все методы в тестовом классе должны быть модульными тестами. Для демонстрации сказанного мы определили метод getTestObject(), который будет использоваться для организации тестов. Поскольку этот метод не имеет атрибута TestMethod, среда Visual Studio не будет трактовать его как модульный тест.

Обратите внимание на добавление оператора using для импорта пространства имен EssentialTools.Models в тестовый класс. Тестовые классы - это всего лишь обычные классы C#, которые совершенно не осведомлены о проекте MVC. Всю "магию" тестирования в проекте обеспечивают атрибуты TestClass и TestMethod.

Как видите, при создании этого метода модульного теста мы следовали шаблону "организация/действие/утверждение" (arrange/act/assert - A/A/A), который был описан в статье "Автоматизированное тестирование".

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

В начале тестового метода мы вызываем метод getTestObject(), который создает экземпляр объекта, предназначенного для тестирования - в данном случае это MinimumDiscountHelper. Кроме того, мы определяем значение total, для которого будет проводиться тестирование. Это раздел организации модульного теста.

В разделе действия теста мы вызываем метод MinimumDiscountHelper.ApplyDiscount() и присваиваем возвращаемый им результат переменной discountedTotal. Наконец, в разделе утверждения теста мы применяем метод Assert.AreEqual() для проверки того, что значение, полученное из метода ApplyDiscount(), составляет 90% общей суммы, указанной в начале.

В классе Assert определен набор статических методов, которые можно использовать в тестах. Этот класс находится в пространстве имен Microsoft.VisualStudio.TestTools.UnitTesting вместе с рядом дополнительных классов, полезных для настройки и выполнения тестов.

Класс Assert является одним из самых часто применяемых, поэтому его важные методы кратко описаны в таблице ниже:

Статические методы класса Assert
Метод Описание
AreEqual<T>(T, T);
AreEqual<T>(T, T, string)

Утверждает, что два объекта типа T имеют одно и то же значение

AreNotEqual<T>(T, T);
AreNotEqual<T>(T, T, string)

Утверждает, что два объекта типа T не имеют одно и то же значение

AreSame<T>(T, T);
AreSame<T>(T, T, string)

Утверждает, что две переменные ссылаются на один и тот же объект

AreNotSame<T> (T, T);
AreNotSame<T>(T, T, string)

Утверждает, что две переменные ссылаются на разные объекты

Fail();
Fail(string)

Отрицательный результат утверждения - никакие условия не проверены

Inconclusive();
Inconclusive(string)

Показывает, что результат модульного теста не может быть однозначно установлен

IsTrue(bool);
IsTrue(bool, string)

Утверждает, что булевское значение равно true - чаще всего используется для оценки выражения, возвращающего булевский результат

IsFalse(bool);
IsFalse(bool, string)

Утверждает, что булевское значение равно false

IsNull(object);
IsNull(object, string)

Утверждает, что переменная не присвоена объектной ссылке

IsNotNull(object);
IsNotNull(object, string)

Утверждает, что переменная присвоена объектной ссылке

IsInstanceOfType(object, Type);
IsInstanceOfType(object, Type, string)

Утверждает, что объект относится к указанному типу или является производным от указанного типа

IsNotInstanceOfType(object,Type);
IsNotInstanceOfType(object, Type, string)

Утверждает, что объект не относится к указанному типу

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

Каждый метод из таблицы имеет перегруженную версию, которая принимает параметр string. В случае отрицательного результата утверждения эта строка помещается в элемент сообщения внутри объекта исключения. Методы AreEqual и AreNotEqual имеют несколько перегруженных версий, предназначенных для сравнения специфических типов. Например, существует версия, которая позволяет сравнивать строки без учета регистра символов.

Одним заслуживающим внимания членом пространства имен Microsoft.VisualStudio.TestTools.UnitTesting является атрибут ExpectedException. Это утверждение, которое дает положительный результат, только если модульный тест генерирует исключение с типом, указанным в параметре ExceptionType. Данный атрибут служит надежным способом обеспечения генерации исключений без необходимости в наличии блоков try...catch внутри кода модульного теста.

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

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EssentialTools.Models;

namespace EssentialTools.Tests
{
    [TestClass]
    public class UnitTest1
    {
        private IDiscountHelper getTestObject()
        {
            return new MinimumDiscountHelper();
        }

        [TestMethod]
        public void Discount_Above_100()
        {
            // arrange (организация)
            IDiscountHelper target = getTestObject();
            decimal total = 200;

            // act (акт)
            var discountedTotal = target.ApplyDiscount(total);

            // assert (утверждение)
            Assert.AreEqual(total * 0.9M, discountedTotal);
        }

        [TestMethod]
        public void Discount_Between_10_And_100()
        {
            // arrange (организация)
            IDiscountHelper target = getTestObject();

            // act (акт)
            decimal TenDollarDiscount = target.ApplyDiscount(10);
            decimal HundredDollarDiscount = target.ApplyDiscount(100);
            decimal FiftyDollarDiscount = target.ApplyDiscount(50);

            // assert (утверждение)
            Assert.AreEqual(5, TenDollarDiscount, "$10 разница");
            Assert.AreEqual(95, HundredDollarDiscount, "$100 разница");
            Assert.AreEqual(45, FiftyDollarDiscount, "$50 разница");
        }

        [TestMethod]
        public void Discount_Less_Than_10()
        {
            // arrange (организация)
            IDiscountHelper target = getTestObject();

            // act (акт)
            decimal discount5 = target.ApplyDiscount(5);
            decimal discount0 = target.ApplyDiscount(0);

            // assert (утверждение)
            Assert.AreEqual(5, discount5);
            Assert.AreEqual(0, discount0);

        }

        [TestMethod]
        [ExpectedException(typeof(ArgumentOutOfRangeException))]
        public void Discount_Negative_Total()
        {
            // arrange (организация) 
            IDiscountHelper target = getTestObject();

            // act (акт)
            target.ApplyDiscount(-1);
        }
    }
}

Прохождение (и не прохождение) модульных тестов

Среда Visual Studio предоставляет окно Test Explorer (Проводник тестов), предназначенное для управления и выполнения тестов. Выберите пункт Windows --> Test Explorer в меню Test среды Visual Studio, чтобы открыть это окно, и щелкните на кнопке Run All (Запустить все) в верхнем левом углу. Вы увидите результаты, похожие на показанные на рисунке ниже:

Выполнение тестов в проекте

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

Реализация функциональной возможности

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

using System;

namespace EssentialTools.Models
{
    public class MinimumDiscountHelper : IDiscountHelper
    {
        public decimal ApplyDiscount(decimal totalParam)
        {
            if (totalParam < 0)
            {
                throw new ArgumentOutOfRangeException();
            }
            else if (totalParam > 100)
            {
                return totalParam * 0.9M;
            }
            else if (totalParam > 10 && totalParam < 100)
            {
                return totalParam - 5;
            }
            else
            {
                return totalParam;
            }
        }
    }
}

Тестирование и исправление кода

Мы преднамеренно оставили ошибку в этом коде, чтобы продемонстрировать работу интерактивного модульного тестирования в Visual Studio, поэтому результат ошибки можно просмотреть, щелкнув на кнопке Run All в окне Test Explorer. Результаты тестирования показаны на рисунке ниже:

Результат реализации функциональной возможности, содержащей ошибку

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

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

В этот момент мы возвращаемся к коду и видим, что ожидаемое поведение не было реализовано должным образом - в частности, скидки для общих сумм, равных 10 или 100, обрабатываются некорректно. Проблема кроется в следующем операторе из класса MinimumDiscountHelper:

else if (totalParam > 10 && totalParam < 100)

Спецификация, с которой мы имеем дело, устанавливает поведение для значений из промежутка между $10 и $100 включительно, но наша реализация исключает эти значения и проверяет только величины, которые больше $10, не учитывая общую сумму, в точности равную $10. Решение выглядит довольно просто и показано в примере ниже - для изменения результата действия оператора if понадобится добавить всего один символ:

// ...
else if (totalParam >= 10 && totalParam <= 100)
// ...

После щелчка на кнопке Run All в окне Test Explorer результаты показывают, что проблема устранена, и все тесты на коде проходят успешно:

Успешное прохождение всех модульных тестов
Пройди тесты
Лучший чат для C# программистов