Добавление модульного тестрования
57ASP.NET --- ASP.NET Web Forms 4.5 --- Добавление модульного тестрования
После запуска тестового приложения в предыдущей статье была возможность увидеть, что базовая функциональность работает. Начальный HTML-контент корректно отправлялся браузеру, а при отправке формы приложение реагировало ожидаемым образом.
Такой вид тестирования может быть очень полезен, но при этом мы проверяем только сквозной сценарий загрузки HTML-контента и отправки формы. Мы также хотим тестировать отдельные компоненты приложения, особенно классы презентаторов, т.к. в случае реального приложения именно в них будет скрываться сложность.
В этой статье мы продемонстрируем применение встроенной в Visual Studio поддержки модульного тестирования. Мы не собираемся детально углубляться в методологии тестирования, а просто хотим показать, что наш подход делает модульное тестирование возможным (и простым).
Создание проекта модульных тестов
Для настройки модульного тестирования удостоверьтесь, что отладчик остановлен, и выберите в меню пункт File --> Add --> New Project. Откроется диалоговое окно Add New Project (Добавление нового проекта). В левой панели найдите категорию Installed --> Visual C# --> Test и выберите шаблон Unit Test Project (Проект модульных тестов), как показано на рисунке ниже:

Укажите в качестве имени TestAspNet45.Tests и щелкните на кнопке OK, чтобы создать проект и добавить его к решению. После этого новый проект можно будет видеть в окне Solution Explorer среды Visual Studio.
Мы добавляем проект модульных тестов только теперь, когда уже построено довольно много функциональности приложения, но обычно это будет делаться в самом начале. Определенные методологии тестирования, наиболее заметной из которых является разработка через тестирование (Test Driven Development - TDD), требуют начинать с определения тестов и затем реализовывать код, необходимый для их прохождения. Мы считаем любое модульное тестирование в проекте Web Forms удачной идеей, и советуем подобрать методологию, которая вписывается в существующий процесс разработки и не превышает потребностей организации в тестировании. Нам приходилось видеть много инициатив по внедрению модульного тестирования, которые завершались провалом из-за того, что их сторонники продвигали их слишком упорно и быстро, превращаясь, в конечном счете, в фанатиков (вы уже знаете, как мы к этому относимся).
Среде Visual Studio необходимо сообщить, какой проект в решении должен запускаться по умолчанию. После создания проекта модульных тестов щелкните правой кнопкой мыши на проекте TestAspNet45 в окне Solution Explorer и выберите в контекстном меню пункт Set as Startup Project (Установить в качестве стартового проекта). Теперь при выборе пункта Start Debugging в меню Debug будет запускаться проект веб-приложения, который запускается всегда - без такого изменения Visual Studio запускает тот проект, который последним выбирался в окне Solution Explorer.
Щелкните правой кнопкой мыши на элементе TestAspNet45.Tests в окне Solution Explorer и выберите в контекстном меню пункт Add Reference (Добавить ссылку), чтобы открыть диалоговое окно Reference Manager (Диспетчер ссылок). Щелкните на элементе Solution (Решение) в левой панели и отметьте флажок рядом с записью TestAspNet45. Это помещает классы, определенные в проекте TestAspNet45, в область видимости для проекта тестов и позволяет использовать их внутри тестов.

Щелкните на кнопке OK для закрытия диалогового окна. Среда Visual Studio обновит элемент References (Ссылки) проекта TestAspNet45.Tests в окне Solution Explorer, чтобы отразить добавление новой ссылки.
Создание модульных тестов
Модульные тесты - это методы, которые проверяют определенное поведение компонента приложения. Множество тестов группируются вместе в тестовые классы, и среда Visual Studio создает такой класс в файле UnitTest1.cs при добавлении нового проекта тестов.
Нам нравится назначать тестовым классам значащие имена, поэтому мы изменяем имя файла UnitTest1.cs, щелкнув на нем правой кнопкой мыши в окне Solution Explorer и выбрав в контекстном меню пункт Rename (Переименовать). Мы собираемся помещать в этот файл тесты для класса RSVPPresenter, так что для него указывается имя RSVPPresenterTests.cs. Когда имя файла изменяется, Visual Studio предлагает обновить имя класса внутри этого файла:

Щелкните на кнопке Yes (Да) и Visual Studio переименует файл и обновит его содержимое так, как показано в примере ниже:
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace TestAspNet45.Tests
{
[TestClass]
public class RSVPPresenterTests
{
[TestMethod]
public void TestMethod1()
{
}
}
}
Важным аспектом этого класса является применение атрибута TestClass, который сообщает Visual Studio о том, что этот класс содержит модульные тесты, а также атрибута TestMethod, обозначающего отдельный тест. В примере ниже определен базовый модульный тест для класса RSVPPresenter:
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq;
using TestAspNet45.Models.Repository;
using TestAspNet45.Models;
using TestAspNet45.Presenters;
using TestAspNet45.Presenters.Results;
namespace TestAspNet45.Tests
{
[TestClass]
public class RSVPPresenterTests
{
class MockRepository : IRepository
{
private List<GuestResponse> mockData = new List<GuestResponse> {
new GuestResponse {Name = "Person1", WillAttend = true},
new GuestResponse {Name = "Person2", WillAttend = false},
};
public IEnumerable<GuestResponse> GetAllResponses()
{
return mockData;
}
public void AddResponse(GuestResponse response)
{
mockData.Add(response);
}
}
[TestMethod]
public void Adds_Object_To_Repository()
{
// Организация
IRepository repo = new MockRepository();
IPresenter<GuestResponse> target = new RSVPPresenter { repository = repo };
GuestResponse dataObject =
new GuestResponse { Name = "TEST", WillAttend = true };
// Действие
IResult result = target.GetResult(dataObject);
// Утверждение
Assert.AreEqual(repo.GetAllResponses().Count(), 3);
Assert.AreEqual(repo.GetAllResponses().Last().Name, "TEST");
Assert.AreEqual(repo.GetAllResponses().Last().WillAttend, true);
}
}
}
Мы определили метод модульного теста по имени Adds_Object_To_Repository(), который выполняет ряд базовых проверок для выяснения, корректно ли класс презентатора добавляет объект данных в хранилище. Реальный модульный тест будет более сложным, и правильно организованное модульное тестирование направлено на проверку многих аспектов класса, но мы просто хотим продемонстрировать результаты использования шаблона MVP. С этой целью код сохраняется простым.
При проведении тестирования реальное хранилище данных часто не должно быть задействовано. Как правило, вместо него применяется имитированная реализация содержащая только средства, которые нужны для выполнения теста. В рассматриваемом примере мы определили имитированную реализацию интерфейса IRepository, которая имеет начальные данные - довольно распространенный и полезный прием во время тестирования.
Для простоты имитированная реализация создана с помощью класса C#. Тем не менее, доступны удобные библиотеки, которые позволяют создавать очень сложные имитированные объекты, делая возможным построение детализированных модульных тестов. Мы отдаем предпочтение библиотеке Moq, которую можно загрузить по адресу code.google.com/p/moq/ или добавить ее в проект модульных тестов с использованием диспетчера пакетов NuGet.
Мы следовали простому тестовому шаблону, который называется "организация/действие/утверждение" (Arrange/Act/Assert). На этапе организации производится подготовка для запуска модульного теста за счет создания целевого объекта и объектов, необходимых тесту. На этапе действия выполняются процессы, которые должны быть протестированы. На этапе утверждения осуществляется проверка, получены ли ожидаемые результаты. Поддержка Visual Studio модульного тестирования предусматривает использование для таких проверок класса Assert, в котором определено множество статических методов, позволяющих оценивать разнообразные виды условий. Мы применяли метод Assert.AreEqual() для проверки, что два значения одинаковы, но доступно много других методов, которые кратко описаны в таблице:
Метод | Описание |
---|---|
AreEqual<T>(T,T) | Утверждает, что два объекта типа T имеют одно и то же значение |
AreNotEqual<T>(T,T) | Утверждает, что два объекта типа T не имеют одно и то же значение |
AreSame<T>(T,T) | Утверждает, что две переменные ссылаются на один и тот же объект |
AreNotSame<T>(T,T) | Утверждает, что две переменные ссылаются на разные объекты |
Fail() | Отрицательный результат утверждения - никакие условия не проверены |
Inconclusive() | Показывает, что результат модульного теста не может быть однозначно установлен |
IsTrue(bool) | Утверждает, что булевское значение равно true - чаще всего используется для оценки выражения, возвращающего булевский результат |
IsFalse(bool) | Утверждает, что булевское значение равно false |
IsNull(object) | Утверждает, что переменная не присвоена объектной ссылке |
IsNotNull(object) | Утверждает, что переменная присвоена объектной ссылке |
IsInstanceOfType (object,Type) | Утверждает, что объект относится к указанному типу или является производным от указанного типа |
IsNotInstanceOfType (object,Type) | Утверждает, что объект не относится к указанному типу |
Чтобы выполнить модульные тесты, выберите пункт меню Test --> Run --> All Tests (Тест --> Запустить --> Все тесты). Среда Visual Studio скомпилирует проекты приложения и модульных тестов, после чего откроет окно Test Explorer (Проводник тестов), которое представлено на рисунке ниже:

У нас определен всего один модульный тест, так что в окне не особо много интересной информации. Тем не менее, в реальном проекте зачастую могут быть сотни тестов, и окно Test Explorer позволяет решить, какие из них запускать, а также отслеживать проблемы, когда тесты не проходят.
Тестирование входных значений
Мы часто находим удобным применение модульных тестов для проверки диапазона значений аргументов, передаваемых методам, которые являются критически важными в работе приложения. Программисты легко делают предположения относительно вида значений, с которыми они должны иметь дело, но они обычно не принимают во внимание, что пользователи вполне могут создать неожиданные ситуации. Нельзя ожидать, что пользователи знают о сделанных нами предположениях, поэтому необходимо посвятить некоторое время обдумыванию проектного решения и тестированию написанного кода.
В качестве простого примера мы протестируем диапазон значений, в которые может быть установлено свойство GuestResponse.WillAttend и эффект, который они оказывают на класс RSVPPresenter. В примере ниже показан новый модульный тест, добавленный в класс RSVPPresenterTests проекта модульных тестов:
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq;
using TestAspNet45.Models.Repository;
using TestAspNet45.Models;
using TestAspNet45.Presenters;
using TestAspNet45.Presenters.Results;
namespace TestAspNet45.Tests
{
[TestClass]
public class RSVPPresenterTests
{
class MockRepository : IRepository
{
// ... для краткости код не показан ...
}
[TestMethod]
public void Adds_Object_To_Repository()
{
// ... для краткости код не показан ...
}
[TestMethod]
public void Handles_WillAttend_Bool_Values()
{
// Arrange
IRepository repo = new MockRepository();
IPresenter<GuestResponse> target = new RSVPPresenter { repository = repo };
bool?[] values = { true, false, null };
// Act & Assert
foreach (bool? testValue in values)
{
GuestResponse dataObject =
new GuestResponse { Name = "TEST", WillAttend = testValue };
IResult result = target.GetResult(dataObject);
Assert.IsInstanceOfType(result, typeof(RedirectResult));
}
}
}
}
В этом тесте этапы действия и утверждения объединены, что позволяет использовать цикл foreach для тестирования возможных значений свойства типа bool? (которыми могут быть true, false и null). Окно Test Explorer после запуска этих тестов показано на рисунке ниже:

Новый модульный тест не прошел. Чтобы посмотреть, что произошло, можно щелкнуть на элементе в разделе Failed Tests (Не прошедшие тесты) окна Test Explorer; отобразятся подробные сведения, представленные на рисунке ниже:

Тест столкнулся с исключением, сгенерированным в классе RSVPPresenter. Проблема возникла, когда свойство WillAttend объекта GuestResponse было равно null. В коде мы просто предполагаем, что свойство WillAttend.Value всегда определено:
using System;
using TestAspNet45.Models;
using TestAspNet45.Models.Repository;
using TestAspNet45.Presenters.Results;
namespace TestAspNet45.Presenters
{
public class RSVPPresenter : IPresenter<GuestResponse>
{
public IRepository repository { get; set; }
IResult IPresenter<GuestResponse>.GetResult()
{
return new DataResult<GuestResponse>(new GuestResponse());
}
IResult IPresenter<GuestResponse>.GetResult(GuestResponse requestData)
{
repository.AddResponse(requestData);
if (requestData.WillAttend.Value)
return new RedirectResult(@"/Content/seeyouthere.html");
else
return new RedirectResult(@"/Content/sorryyoucantcome.html");
}
}
}
Мы не замечали этого ранее, т.к. были защищены от значений null проверкой достоверности данных, которая выполнялась при отправке формы. Мы могли бы положиться на упомянутую защиту, но по нашему опыту это неудачная идея. В какой-то момент в будущем понадобится изменить способ проверки достоверности данных, и тогда значения null начнут проникать в класс RSVPPresenter. Гораздо эффективнее обеспечить, чтобы класс RSVPPresenter мог иметь дело с полным диапазоном значений и, таким образом, не вызывал проблем в будущем.
Существует много способов решения указанной проблемы, но мы избрали подход с генерацией исключения, когда получены значения null, как показано в примере ниже:
using System;
using TestAspNet45.Models;
using TestAspNet45.Models.Repository;
using TestAspNet45.Presenters.Results;
namespace TestAspNet45.Presenters
{
public class RSVPPresenter : IPresenter<GuestResponse>
{
public IRepository repository { get; set; }
IResult IPresenter<GuestResponse>.GetResult()
{
return new DataResult<GuestResponse>(new GuestResponse());
}
IResult IPresenter<GuestResponse>.GetResult(GuestResponse requestData)
{
repository.AddResponse(requestData);
if (!requestData.WillAttend.HasValue)
throw new System.ArgumentNullException("WillAttend равно null");
if (requestData.WillAttend.Value)
return new RedirectResult(@"/Content/seeyouthere.html");
else
return new RedirectResult(@"/Content/sorryyoucantcome.html");
}
}
}
Теперь можно модифицировать модульные тесты, чтобы отразить это изменение:
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq;
using TestAspNet45.Models.Repository;
using TestAspNet45.Models;
using TestAspNet45.Presenters;
using TestAspNet45.Presenters.Results;
namespace TestAspNet45.Tests
{
[TestClass]
public class RSVPPresenterTests
{
class MockRepository : IRepository
{
// ... для краткости код не показан ...
}
[TestMethod]
public void Adds_Object_To_Repository()
{
// ... для краткости код не показан ...
}
[TestMethod]
public void Handles_WillAttend_Bool_Values()
{
// Arrange
IRepository repo = new MockRepository();
IPresenter<GuestResponse> target = new RSVPPresenter { repository = repo };
bool?[] values = { true, false };
// Act & Assert
foreach (bool? testValue in values)
{
GuestResponse dataObject =
new GuestResponse { Name = "TEST", WillAttend = testValue };
IResult result = target.GetResult(dataObject);
Assert.IsInstanceOfType(result, typeof(RedirectResult));
}
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Handles_WillAttend_Null_Values()
{
// Arrange
IRepository repo = new MockRepository();
IPresenter<GuestResponse> target = new RSVPPresenter { repository = repo };
// Act
GuestResponse dataObject = new GuestResponse { Name = "TEST", WillAttend = null };
IResult result = target.GetResult(dataObject);
}
}
}
Тест был разделен на две части, чтобы значения true и false проверялись методом Handles_WillAttend_Bool_Values(), а значение null - методом Handles_WillAttend_Null_Values(). К новому модульному тесту применен атрибут ExpectedException, который сообщает Visual Studio о том, что данный тест вызовет исключение. Тест для значения null выделен в отдельный метод, поскольку при возникновении исключения выполнение тестового метода останавливается, а мы не хотим спутать исключение ArgumentNullException, сгенерированное где-то в другом месте, с исключением, которое было только что добавлено.
Если запустить модульные тесты сейчас, будут получены результаты, показанные на рисунке ниже:

Использование шаблона MVP означает возможность изолирования класса RSVPPresenter от остальной части приложения и проверки его со значениями, которые просто не могут быть получены при тестировании приложения как единого целого. В реальном проекте мы бы добавили в файл \Pages\Default.aspx код для обработки исключения, но в настоящей статье мы этого делать не будем, т.к. хотим сосредоточиться на реализации остальных частей шаблона MVP и приложения.