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


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

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

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

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

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

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

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

  • Итерация 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. Макеты Rhino Mocks можно скачать по ссылке http://ayende.com/projects/rhino-mocks.aspx.
  3. Typemock Isolator — это коммерческая платформа. Пробную версию можно скачать по ссылке http://www.typemock.com/.

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

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

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

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

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

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

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

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

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

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

Примечание

Как правило, требуется, чтобы структура папок тестового проекта соответствовала структуре папок проекта 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.vb

Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports Moq
Imports System.Web.Mvc

<TestClass()> _
Public Class ContactManagerServiceTest

    Private _mockRepository As Mock(Of IContactManagerRepository)
    Private _modelState As ModelStateDictionary
    Private _service As IContactManagerService

    <TestInitialize()> _
    Public Sub Initialize()
        _mockRepository = New Mock(Of IContactManagerRepository)()
        _modelState = New ModelStateDictionary()
        _service = New ContactManagerService(new ModelStateWrapper(_modelState), _mockRepository.Object)
    End Sub

    <TestMethod()> _
    Public Sub CreateContact()
        ' Arrange
        Dim contactToCreate = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "steve@somewhere.com")

        ' Act
        Dim result = _service.CreateContact(contactToCreate)

        ' Assert
        Assert.IsTrue(result)
    End Sub

    <TestMethod()> _
    Public Sub CreateContactRequiredFirstName()
        ' Arrange
        Dim contactToCreate = Contact.CreateContact(-1, String.Empty, "Walther", "555-5555", "steve@somewhere.com")

        ' Act
        Dim result = _service.CreateContact(contactToCreate)

        ' Assert
        Assert.IsFalse(result)
        Dim [error] = _modelState("FirstName").Errors(0)
        Assert.AreEqual("First name is required.", [error].ErrorMessage)
    End Sub

    <TestMethod()> _
    Public Sub CreateContactRequiredLastName()
        ' Arrange
        Dim contactToCreate = Contact.CreateContact(-1, "Stephen", String.Empty, "555-5555", "steve@somewhere.com")

        ' Act
        Dim result = _service.CreateContact(contactToCreate)

        ' Assert
        Assert.IsFalse(result)
        Dim [error] = _modelState("LastName").Errors(0)
        Assert.AreEqual("Last name is required.", [error].ErrorMessage)
    End Sub

    <TestMethod()> _
    Public Sub CreateContactInvalidPhone()
        ' Arrange
        Dim contactToCreate = Contact.CreateContact(-1, "Stephen", "Walther", "apple", "steve@somewhere.com")

        ' Act
        Dim result = _service.CreateContact(contactToCreate)

        ' Assert
        Assert.IsFalse(result)
        Dim [error] = _modelState("Phone").Errors(0)
        Assert.AreEqual("Invalid phone number.", [error].ErrorMessage)
    End Sub

    <TestMethod()> _
    Public Sub CreateContactInvalidEmail()
        ' Arrange
        Dim contactToCreate = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "apple")

        ' Act
        Dim result = _service.CreateContact(contactToCreate)

        ' Assert
        Assert.IsFalse(result)
        Dim [error] = _modelState("Email").Errors(0)
        Assert.AreEqual("Invalid email address.", [error].ErrorMessage)
    End Sub
End Class

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

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

_mockRepository = New Mock(Of IContactManagerRepository)()

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

Примечание

При использовании платформы Moq существует различие между _mockRepository и _mockRepository.Object. Первый относится к классу Mock(Of 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.NET приложения MVC управляют потоком взаимодействия с пользователем. При тестировании контроллера необходимо проверить, возвращает ли контроллер правильный результат действия, и просмотреть данные. Также может потребоваться проверить, взаимодействует ли контроллер с классами модели ожидаемым образом.

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

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

_service = New Mock(Of IContactManagerService)()

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

_service.Expect( Function(s) s.CreateContact(contactToCreate) ).Returns(True)

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

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

_service.Expect( Function(s) s.CreateContact(contactToCreate) ).Returns(False)

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

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

Return View()

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

Return View("Create")

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

Листинг 2. Контроллеры\ContactControllerTest.vb

Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports Moq
Imports System.Web.Mvc

<TestClass()> _
Public Class ContactControllerTest

    Private _service As Mock(Of IContactManagerService)

    <TestInitialize()> _
    Public Sub Initialize()
        _service = New Mock(Of IContactManagerService)()
    End Sub

    <TestMethod()> _
    Public Sub CreateValidContact()
        ' Arrange
        Dim contactToCreate = New Contact()
        _service.Expect(Function(s) s.CreateContact(contactToCreate)).Returns(True)
        Dim controller = New ContactController(_service.Object)

        ' Act
        Dim result = CType(controller.Create(contactToCreate), RedirectToRouteResult)

        ' Assert
        Assert.AreEqual("Index", result.RouteValues("action"))
    End Sub

    <TestMethod()> _
    Public Sub CreateInvalidContact()
        ' Arrange
        Dim contactToCreate = New Contact()
        _service.Expect(Function(s) s.CreateContact(contactToCreate)).Returns(False)
        Dim controller = New ContactController(_service.Object)

        ' Act
        Dim result = CType(controller.Create(contactToCreate), ViewResult)

        ' Assert
        Assert.AreEqual("Create", result.ViewName)
    End Sub

End Class

Итоги

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

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

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