Модульное тестирование ASP.NET MVC 3 приложений в Visual Studio 2010. Часть 1
В данной статье мы детально рассмотрим, как писать модульные тесты (unit tests) для ASP.NET MVC 3 проектов.
Содержание:
- Вступление
- Модульные тесты для MVC роутов
- Тестирование входящих роутов
- Использование фреймворка Moq
- Второй способ тестирования
- Тестирование исходящих роутов
Вступление
Как вы знаете, архитектура ASP.NET MVC была задумана таким образом, чтобы тестирование функционала было легким, что подразумевает разделение бизнес логики от ее представления. В шаблоне MVC вся основная логика находится внутри контроллеров, поэтому разработчики могут создавать и выполнять код внутри контроллеров, как любой дугой .NET класс. Это позволяет тестировать контроллеры с помощью модульных тестов практически в такой же манере, как и любой другой POCO (Plain Old CLR Object) класс.
Две очень важные возможности в стиле test-driven development (TDD) были представлены в ASP.NET MVC 3. Первая возможность – это возможность тестирования Razor представления без необходимости его отображения в браузере. Текущий релиз содержит два новых сервиса (IControllerActivator и IViewActivator) и четыре ранее реализованных сервиса (Model validation provider, Model metadata provider, Value provider и Model binder) и улучшения, связанные с разрешением зависимостей и Common Service Locator. Все эти возможности (особенно поддержка dependency injection) позволяют легко разделять различные компоненты и их тестировать.
Примечание. Для лучшего понимания материала, представленного в статье, вам желательно ознакомится с предыдущими статьями автора, посвященным MVC 3 dependency injection на dotnetslackers.
Инструменты и библиотеки, которые будут использоваться в статье:
- Windows 7;
- .NET 4.0;
- Visual Studio 2010;
- ASP.NET MVC 3;
- Тестовая база Microsoft Northwind;
- Библиотека для создания моков Moq 4.0 (адрес для загрузки: http://code.google.com/p/moq/downloads/list);
- Ninject 2.0 (адрес для загрузки: http://github.com/ninject/ninject/downloads);
- Ninject расширения для ASP.NET MVC (адрес для загрузки: https://github.com/ninject/ninject.web.mvc).
Модульные тесты для MVC роутов
Роутинг играет важную роль в разработке проектов на ASP.NET MVC. Но конфигурирование роутов может стать непростым делом, особенно, если их у вас достаточно много. После того, как вы заполнили первоначальную коллекцию RouteTable.Routes и после этого добавили или изменили какой-нибудь роут, вы можете легко что-то «поломать». Использование модульных тестов в данной ситуации поможет вам быть уверенным в том, что изменения не приведут к сбоям.
Примечание. Как можно «поломать» сайт из-за системы роутов?
Например, вы определили маску роута таким образом: "{product}/{id}". При этом вы убедились, что адреса "kindle/102", "notebook/234" и т.д. работают. Спустя некоторое время вы добавили еще один роут с маской: "{action}/{userId}", подразумевая такие адреса как "edit/34567" и "delete/34456". В результате добавления такого роута будет выдано исключение, так как система роутов не будет знать, какой из роутов необходимо использовать.
Давайте рассмотрим несколько сценариев тестирования роутов более детально.
Тестирование входящих роутов
Тестирование роутов означает загрузку тестовых адресов в систему роутинга и анализ значений роут, которые возвращает система. В самом базовом варианте тест имеет такой вид:
[TestMethod] public void TestCertainRoute() { //Arrange: var routes = new RouteCollection(); MvcApplication.RegisterRoutes(routes); HttpContextBase testContext = manage to get an instance somehow somewhere // Act:run the routing engine against this HttpContextBase RouteData routeData = routes.GetRouteData(testContext); // Assert Assert.IsNotNull(routeData); //other related assertions... }
Примечание: шаблон AAA (взято здесь).
Использование шаблона Arrange-Act-Assert (AAA) при написании unit тестов намного повышает шансы других разработчиков понять ваш код. Данный шаблон всего лишь разделяет и группирует код теста на 3 секции, придавая удобную для чтения структуру модульного теста:
1) Arrange - выставление начальных условий.
2) Act - отработка тестируемого функционала.
3) Assert - сверка ожидаемых значений с полученными.
Пример использования шаблона ААА:
[Test] public void TestTranslate() { // Arrange. // // Здесь мы можем setup-ить наши Mock объекты, // например создать Mock-словарь, который потом // передать в качестве аргумента в конструктор. ITranslator translator = new EngRusTranslator(); // Act. // // Отработка тестируемого функционала. string result = translator.Translate("Hello, World!"); // Assert. // // Проверка. Assert или Verify метод. Assert.AreEqual("Привет, Мир!", result); }
Первый момент, на который стоит обратить внимание, это возможность конфигурирования системы роутов с помощью публичного статического метода RegisterRoutes(), который находится в файле Global.asax.cs. Этот метод ответственный за конфигурирование системы роутов и возвращение заполненной коллекции роутов.
Более сложной задачей является получение экземпляра класа HttpContextBase, с учетом того, что мы не хотим в тестовом коде использовать реальный контекст веб-сервера. Одним из популярных методов является создание специальных объектов (моков), которые возвращают объекты типа HttpContextBase. После того, как мы получим информацию о системе роутов, мы можем приступить к их тестированию.
Использование фреймворка Moq
Давайте посмотрим на код теста с использованием фреймворка Moq.
[TestMethod] public void TestCertainRoute() { var routes = new RouteCollection(); MvcApplication.RegisterRoutes(routes); var mockHttpContext = MakeMockHttpContext("~/"); RouteData routeData = routes.GetRouteData(mockHttpContext.Object); Assert.IsNotNull(routeData, "NULL RouteData was returned"); //other related assertions... }
При тестировании роутов понимание двух структур – RouteCollection и RouteData является обязательным. Эти две структуры являются обязательными компонентами ASP.NET MVC. В библиотеке Moq вы найдете метод SetupMockHttpContext(), который является очень важным. Рассмотрим его код детальнее:
private static Mock<HttpContextBase> MakeMockHttpContext(string url) { var mockHttpContext = new Mock<HttpContextBase>(); // Mock the request var mockRequest = new Mock<HttpRequestBase>(); mockHttpContext.Setup(x => x.Request).Returns(mockRequest.Object); mockRequest.Setup(x => x.AppRelativeCurrentExecutionFilePath).Returns(url); // Mock the response var mockResponse = new Mock<HttpResponseBase>(); mockHttpContext.Setup(x => x.Response).Returns(mockResponse.Object); mockResponse.Setup(x => x.ApplyAppPathModifier(It.IsAny<string>())) .Returns<string>(x => x); return mockHttpContext; }
С помощью Moq мы имеем возможность создавать фейковые экземпляры классов HttpContextBase, HttpRequestBase и HttpResponseBase, которые являются публичными абстрактными классами.
Второй способ тестирования
Кроме рассмотренного выше способа, есть еще один способ создания экземпляра класса HttpContextBase. Вы можете унаследоваться от класса HttpContextBase и реализовать только те методы и свойства, которые вы используете.
Ниже приведена реализация минимальная реализация HttpContextBase - достаточная для тестирования роутов (реализованы лишь те методы, которые использует система роутов).
public class TestHttpContext : HttpContextBase { TestHttpRequest testRequest; TestHttpResponse testResponse; public override HttpRequestBase Request { get { return testRequest; } } public override HttpResponseBase Response { get { return testResponse; } } public TestHttpContext(string url) { testRequest = new TestHttpRequest() { _AppRelativeCurrentExecutionFilePath = url }; testResponse = new TestHttpResponse(); } class TestHttpRequest : HttpRequestBase { public string _AppRelativeCurrentExecutionFilePath { get; set; } public override string AppRelativeCurrentExecutionFilePath { get { return _AppRelativeCurrentExecutionFilePath; } } public override string ApplicationPath { get { return null; } } public override string PathInfo { get { return null; } } public override NameValueCollection ServerVariables { get { return null; } } } class TestHttpResponse : HttpResponseBase { public override string ApplyAppPathModifier(string x) { return x; } } }
Вы, наверняка, увидели много общего между этим кодом и приведенным выше кодом метода MakeMockHttpContext. Но этот код требует более внимательного подхода, но также имеет смысл в некоторых случаях. Между прочим, чтобы научиться писать такой код, можно изучить файл AccountControllerTest.cs, который генерируется автоматически в тестовом проекте.
Теперь, имея реализацию класса HttpContextBase, можно переписать наш код с использованием класса TestHttpContext:
[TestMethod] public void TestCertainRoute(){ // Arrange RouteCollection routeConfig = new RouteCollection(); MvcApplication.RegisterRoutes(routeConfig); HttpContextBase testContext = new TestHttpContext("~/"); // Act RouteData routeData = routeConfig.GetRouteData(testContext); // Assert Assert.IsNotNull(routeData, "NULL RouteData was returned"); Assert.IsNotNull(routeData.Route, "No route was matched"); Assert.AreEqual("Home", routeData.Values["controller"], "Wrong controller"); Assert.AreEqual("Index", routeData.Values["action"], "Wrong action"); }
Теперь давайте рассмотрим как тестировать исходящие роуты.
Тестирование исходящих роутов
Тестирование исходящих роутов отличается от тестирования входящих роутов, так как конкретный URL ассоциируется с набором значений RouteData, что не гарантирует, что тот же набор значений RouteData будет ассоциирован с тем же URL.
Но к счастью, тестирование исходящих роутов также простое. Для начала нам необходимо воспользоваться классом System.Web.MVC.UrlHelper для создания специального набора значений. Все, что нам нужно, это получить доступ к роутам и пути, по которому исполняется наше приложение.
С помощью Moq мы можем написать тесты таким образом:
[TestMethod] public void Edit_Products_ID_10_Test() { string result = SetupUrlViaMocks( new { controller = "Products", action = "Edit", id = 10 } ); Assert.AreEqual("/Products/Edit/10", result); }
Этот код очень простой. Реализация метода SetupUrlViaMocks имеет следующий вид:
private string SetupUrlViaMocks(object values) { RouteCollection routeConfig = new RouteCollection(); MvcApplication.RegisterRoutes(routeConfig); var mockContext = MakeMockHttpContext(null); RequestContext context = new RequestContext(mockContext.Object, new RouteData()); return UrlHelper.GenerateUrl(null, null, null, new RouteValueDictionary(values), routeConfig, context, true); }
Обратите внимание, что MakeMockHttpContext() был определен в предыдущем примере. Также необходимо обратить внимание на класс UrlHelper.
Небольшая справка по классу UrlHelper, который содержит методы построения URL-адресов для MVC ASP.NET в приложении. Предоставляет следующие методы-расширения для работы с URL-адресами:
- Action: Этот метод создает URL-адрес, который сопоставляется методу действия.
- RouteUrl: Этот метод создает URL-адрес, который сопоставляется маршруту.
- Content: Этот метод создает URL-путь к ресурсу на основе виртуального (относительного) пути к ресурсу.
- Encode: Этот метод кодирует специальные знаки в заданном URL-адресе в эквиваленты сущности знака.
В следующей части рассмотрим тестирование модели и контроллеров, а также более детально познакомимся с библиотекой Moq.
Компании из статьи
Microsoft Украина | Украинское подразделение компании Microsoft. |