Поделиться через


Итерация 5. Создание модульных тестов (C#)

от Майкрософт

Скачивание кода

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

Создание приложения MVC для управления контактами ASP.NET (C#)

В этой серии учебников мы создадим все приложение управления контактами от начала до конца. Приложение Диспетчер контактов позволяет хранить контактные данные ( имена, номера телефонов и адреса электронной почты) для списка людей.

Мы создаем приложение с помощью нескольких итераций. С каждой итерацией мы постепенно улучшаем приложение. Цель этого подхода с несколькими итерациями — дать возможность понять причину каждого изменения.

  • Итерация 1. Создание приложения. В первой итерации мы создадим диспетчер контактов самым простым способом. Добавлена поддержка базовых операций с базами данных: создание, чтение, обновление и удаление (CRUD).

  • Итерация 2. Сделайте приложение красивым. В этой итерации мы улучшаем внешний вид приложения, изменив ASP.NET по умолчанию представление MVC master страницу и каскадную таблицу стилей.

  • Итерация 3. Добавление проверки формы. В третьей итерации мы добавим базовую проверку формы. Мы запрещаем пользователям отправлять форму без заполнения обязательных полей формы. Мы также проверяем адреса электронной почты и номера телефонов.

  • Итерация 4. Сделайте приложение слабосвязанным. В этой четвертой итерации мы воспользуемся преимуществами нескольких шаблонов проектирования программного обеспечения, чтобы упростить обслуживание и изменение приложения Диспетчера контактов. Например, мы рефакторинг приложения для использования шаблона репозитория и шаблона внедрения зависимостей.

  • Итерация 5. Создание модульных тестов. В пятой итерации мы упростим обслуживание и изменение приложения, добавив модульные тесты. Мы имитируем классы модели данных и создаем модульные тесты для наших контроллеров и логики проверки.

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

  • Итерация 7. Добавление функциональных возможностей Ajax. В седьмой итерации мы повышаем скорость реагирования и производительность приложения, добавляя поддержку Ajax.

Эта итерация

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

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

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

Например, в один прекрасный день руководитель может попросить вас добавить новую функцию в диспетчер контактов. Она хочет, чтобы вы добавили поддержку для групп контактов. Она хочет, чтобы пользователи могли упорядочивать свои контакты по группам, таким как "Друзья", "Бизнес" и т. д.

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

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

Модульный тест используется для тестирования отдельного блока кода. Эти единицы кода меньше, чем целые уровни приложения. Как правило, модульный тест используется для проверки того, ведет ли определенный метод в коде ожидаемым образом. Например, можно создать модульный тест для метода CreateContact(), предоставляемого классом ContactManagerService.

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

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

Примечание

Существует множество платформ модульного тестирования, включая NUnit, xUnit.net и MbUnit. В этом руководстве мы используем платформу модульного тестирования, включенную в Visual Studio. Однако можно так же легко использовать одну из этих альтернативных платформ.

Что тестируется

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

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

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

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

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

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

Примечание

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

Примечание

ASP.NET MVC — это обработчик представлений веб-формы. Хотя обработчик представлений веб-формы зависит от веб-сервера, другие обработчики представлений могут не быть.

Использование макета объектной платформы

При создании модульных тестов почти всегда необходимо воспользоваться преимуществами платформы макета объектов. Платформа Mock Object позволяет создавать макеты и заглушки для классов в приложении.

Например, можно использовать платформу Mock Object для создания макета версии класса репозитория. Таким образом, в модульных тестах можно использовать макетный класс репозитория вместо класса реального репозитория. Использование макетного репозитория позволяет избежать выполнения кода базы данных при выполнении модульного теста.

Visual Studio не включает платформу макетов объектов. Однако для платформы .NET Framework доступно несколько коммерческих и открытый код макетных объектов:

  1. Moq — эта платформа доступна по лицензии открытый код BSD. Moq можно скачать по ссылке https://code.google.com/p/moq/.
  2. Rhino Mocks — эта платформа доступна по лицензии открытый код BSD. Макеты носорогов можно скачать по ссылке http://ayende.com/projects/rhino-mocks.aspx.
  3. Typemock Isolator — это коммерческая платформа. Пробную версию можно скачать по ссылке http://www.typemock.com/.

В этом руководстве я решил использовать Moq. Однако вы можете с таким же легкостью использовать макеты Rhino или Typemock Isolator для создания объектов макета для приложения Диспетчера контактов.

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

  1. .
  2. Перед распакуйте скачиваемый файл правой кнопкой мыши и нажмите кнопку Разблокировать (см. рис. 1).
  3. Распакуйте скачиваемые файлы.
  4. Добавьте ссылку на сборку Moq, щелкнув правой кнопкой мыши папку References в проекте ContactManager.Tests и выбрав Добавить ссылку. На вкладке Обзор перейдите в папку, в которой распаковали Moq, и выберите Moq.dll сборку. Нажмите кнопку ОК .
  5. После выполнения этих действий папка References должна выглядеть, как на рисунке 2.

Разблокировка Moq

Рис. 01. Разблокировка Moq(щелкните для просмотра полноразмерного изображения)

Ссылки после добавления Moq

Рис. 02. Ссылки после добавления Moq(Щелкните для просмотра полноразмерного изображения)

Создание модульных тестов для уровня служб

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

Создайте папку с именем Models в проекте ContactManager.Tests. Затем щелкните правой кнопкой мыши папку Models и выберите Добавить, Создать тест. Откроется диалоговое окно Добавление нового теста , показанное на рис. 3. Выберите шаблон Модульный тест и назовите новый тест ContactManagerServiceTest.cs. Нажмите кнопку ОК , чтобы добавить новый тест в тестовый проект.

Примечание

Как правило, требуется, чтобы структура папок тестового проекта соответствовала структуре папок проекта ASP.NET MVC. Например, тесты контроллеров помещаем в папку Контроллеры, тесты модели — в папку Models и т. д.

Models\ContactManagerServiceTest.cs

Рис. 03. Models\ContactManagerServiceTest.cs(Щелкните для просмотра полноразмерного изображения)

Сначала мы хотим протестировать метод CreateContact(), предоставляемый классом ContactManagerService. Мы создадим следующие пять тестов:

  • CreateContact() — проверяет, что CreateContact() возвращает значение true при передаче допустимого контакта в метод .
  • CreateContactRequiredFirstName() — проверяет, добавляется ли сообщение об ошибке в состояние модели при передаче контакта с отсутствующим именем в метод CreateContact().
  • CreateContactRequiredLastName() — проверяет, добавляется ли сообщение об ошибке в состояние модели при передаче контакта с отсутствующим именем в метод CreateContact().
  • CreateContactInvalidPhone() — проверяет, добавляется ли сообщение об ошибке в состояние модели при передаче контакта с недопустимым номером телефона в метод CreateContact().
  • CreateContactInvalidEmail() — проверяет, добавляется ли сообщение об ошибке в состояние модели при передаче контакта с недопустимым адресом электронной почты в метод CreateContact().

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

Код для этих тестов содержится в листинге 1.

Листинг 1. Models\ContactManagerServiceTest.cs

using System.Web.Mvc;
using ContactManager.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace ContactManager.Tests.Models
{
    [TestClass]
    public class ContactManagerServiceTest
    {
        private Mock<IContactManagerRepository> _mockRepository;
        private ModelStateDictionary _modelState;
        private IContactManagerService _service;

        [TestInitialize]
        public void Initialize()
        {
            _mockRepository = new Mock<IContactManagerRepository>();
            _modelState = new ModelStateDictionary();
            _service = new ContactManagerService(new ModelStateWrapper(_modelState), _mockRepository.Object);
        }

        [TestMethod]
        public void CreateContact()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "steve@somewhere.com");

            // Act
            var result = _service.CreateContact(contact);
        
            // Assert
            Assert.IsTrue(result);
        }

        [TestMethod]
        public void CreateContactRequiredFirstName()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, string.Empty, "Walther", "555-5555", "steve@somewhere.com");

            // Act
            var result = _service.CreateContact(contact);

            // Assert
            Assert.IsFalse(result);
            var error = _modelState["FirstName"].Errors[0];
            Assert.AreEqual("First name is required.", error.ErrorMessage);
        }

        [TestMethod]
        public void CreateContactRequiredLastName()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, "Stephen", string.Empty, "555-5555", "steve@somewhere.com");

            // Act
            var result = _service.CreateContact(contact);

            // Assert
            Assert.IsFalse(result);
            var error = _modelState["LastName"].Errors[0];
            Assert.AreEqual("Last name is required.", error.ErrorMessage);
        }

        [TestMethod]
        public void CreateContactInvalidPhone()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, "Stephen", "Walther", "apple", "steve@somewhere.com");

            // Act
            var result = _service.CreateContact(contact);

            // Assert
            Assert.IsFalse(result);
            var error = _modelState["Phone"].Errors[0];
            Assert.AreEqual("Invalid phone number.", error.ErrorMessage);
        }

        [TestMethod]
        public void CreateContactInvalidEmail()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "apple");

            // Act
            var result = _service.CreateContact(contact);

            // Assert
            Assert.IsFalse(result);
            var error = _modelState["Email"].Errors[0];
            Assert.AreEqual("Invalid email address.", error.ErrorMessage);
        }
    }
}

Так как мы используем класс Contact в листинге 1, необходимо добавить ссылку на Microsoft Entity Framework в наш тестовый проект. Добавьте ссылку на сборку System.Data.Entity.

В листинге 1 содержится метод Initialize(), украшенный атрибутом [TestInitialize]. Этот метод вызывается автоматически перед выполнением каждого из модульных тестов (он вызывается 5 раз непосредственно перед каждым модульным тестом). Метод Initialize() создает репозиторий макета со следующей строкой кода:

_mockRepository = new Mock<IContactManagerRepository>();

В этой строке кода используется платформа Moq для создания макетного репозитория из интерфейса IContactManagerRepository. Макет репозитория используется вместо фактического Объекта EntityContactManagerRepository, чтобы избежать доступа к базе данных при выполнении каждого модульного теста. Макет репозитория реализует методы интерфейса IContactManagerRepository, но методы на самом деле ничего не делают.

Примечание

При использовании платформы Moq существует различие между _mockRepository и _mockRepository.Object. Первый относится к классу Mock<IContactManagerRepository> , который содержит методы для указания поведения репозитория макетов. Последний относится к фактическому репозиторию макетов, который реализует интерфейс IContactManagerRepository.

Макет репозитория используется в методе Initialize() при создании экземпляра класса ContactManagerService. Все отдельные модульные тесты используют этот экземпляр класса ContactManagerService.

В листинге 1 содержится пять методов, соответствующих каждому из модульных тестов. Каждый из этих методов дополнен атрибутом [TestMethod]. При выполнении модульных тестов вызывается любой метод с этим атрибутом. Иными словами, любой метод, украшенный атрибутом [TestMethod], является модульным тестом.

Первый модульный тест с именем CreateContact() проверяет, возвращается ли вызов CreateContact() значение true при передаче допустимого экземпляра класса Contact в метод . Тест создает экземпляр класса Contact, вызывает метод CreateContact() и проверяет, возвращает ли CreateContact() значение true.

Остальные тесты проверяют, что при вызове метода CreateContact() с недопустимым contact метод возвращает значение false, а ожидаемое сообщение об ошибке проверки добавляется в состояние модели. Например, тест CreateContactRequiredFirstName() создает экземпляр класса Contact с пустой строкой для его свойства FirstName. Затем вызывается метод CreateContact() с недопустимым contact. Наконец, тест проверяет, возвращает ли CreateContact() значение false, а состояние модели содержит ожидаемое сообщение об ошибке проверки "Требуется имя".

Модульные тесты, приведенные в листинге 1, можно запустить, выбрав в меню Пункт Тест, Выполнить, Все тесты в решении (CTRL+R, A). Результаты тестов отображаются в окне Результаты теста (см. рис. 4).

Результаты теста

Рис. 04. Результаты теста (щелкните для просмотра полноразмерного изображения)

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

ASP. Приложение NETMVC управляет потоком взаимодействия с пользователем. При тестировании контроллера необходимо проверить, возвращает ли контроллер правильный результат действия, и просмотреть данные. Также может потребоваться проверить, взаимодействует ли контроллер с классами модели ожидаемым образом.

Например, в листинге 2 содержатся два модульных теста для метода Create() контроллера контактов. Первый модульный тест проверяет, что при передаче допустимого контакта методу Create() метод Create() перенаправляется на действие Индекс. Другими словами, при передаче допустимого контакта метод Create() должен возвращать RedirectToRouteResult, представляющий действие Index.

Мы не хотим тестировать уровень служб ContactManager при тестировании уровня контроллера. Поэтому мы имитируем уровень служб с помощью следующего кода в методе Initialize:

_service = new Mock();

В модульном тесте CreateValidContact() мы макетируем поведение вызова метода CreateContact() уровня служб с помощью следующей строки кода:

_service.Expect(s => s.CreateContact(contact)).Returns(true);

Эта строка кода приводит к тому, что макет службы ContactManager возвращает значение true при вызове метода CreateContact(). Макетируя уровень служб, мы можем протестировать поведение контроллера без необходимости выполнять какой-либо код на уровне служб.

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

_service.Expect(s => s.CreateContact(contact)).Returns(false);

Если метод Create() ведет себя должным образом, он должен возвращать представление Create, когда уровень служб возвращает значение false. Таким образом, контроллер может отображать сообщения об ошибках проверки в представлении "Создать", и пользователь имеет возможность исправить эти недопустимые свойства контакта.

Если вы планируете создавать модульные тесты для контроллеров, необходимо возвращать явные имена представлений из действий контроллера. Например, не возвращайте представление, подобное следующему:

return View();

Вместо этого верните представление следующим образом:

return View("Create");

Если при возврате представления не указано явное значение, свойство ViewResult.ViewName возвращает пустую строку.

Листинг 2. Controllers\ContactControllerTest.cs

using System.Web.Mvc;
using ContactManager.Controllers;
using ContactManager.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace ContactManager.Tests.Controllers
{
    [TestClass]
    public class ContactControllerTest
    {
        private Mock<IContactManagerService> _service;

        [TestInitialize]
        public void Initialize()
        {
            _service = new Mock<IContactManagerService>();
        }

        [TestMethod]
        public void CreateValidContact()
        {
            // Arrange
            var contact = new Contact();
            _service.Expect(s => s.CreateContact(contact)).Returns(true);
            var controller = new ContactController(_service.Object);
        
            // Act
            var result = (RedirectToRouteResult)controller.Create(contact);

            // Assert
            Assert.AreEqual("Index", result.RouteValues["action"]);
        }

        [TestMethod]
        public void CreateInvalidContact()
        {
            // Arrange
            var contact = new Contact();
            _service.Expect(s => s.CreateContact(contact)).Returns(false);
            var controller = new ContactController(_service.Object);

            // Act
            var result = (ViewResult)controller.Create(contact);

            // Assert
            Assert.AreEqual("Create", result.ViewName);
        }

    }
}

Итоги

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

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

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