Модульное тестирование ASP.NET MVC 3 приложений в Visual Studio 2010. Часть 2

воскресенье, 3 июля 2011, Александр Краковецкий

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

Содержание

  • Немного о модульном тестировании моделей и контроллеров;
  • Введение в библиотеку для создания моков - Moq;
    • Загрузка и установка Moq;
    • Что может быть реализовано как мок?;
    • Mock;
    • It;
    • MockBehavior;
    • MockFactory;
    • Проверка.

Немного о модульном тестировании моделей и контроллеров

Так как тестирование бизнес объектов (модели) в ASP.NET MVC ничем не отличается от тестирования бизнес объектов в любом другом типе приложений, то этот вопрос мы освещать не будем.

В то же время тестированию контроллеров в стиле TDD посвящено много внимания в фреймворке ASP.NET MVC.

На тестировании простых action методов мы особо останавливаться не будем, однако есть много нюансов связанных с тестированием реальных сценариев. Во-первых, есть два важных, и даже в некоторой степени сложных момента, связанных с тестированием классов HttpContext, HttpRequest, HttpResponse, о чем мы уже упоминали раньше. Второй момент – это тестирование работы с базой данных (или веб-сервисов). Этим вопросам уделим особое внимание.

Если action метод возвращает общий ActionResult (а не специализированый, такой как, например, ViewResult), то нам необходимо будет приводить этот результат к нужному нам типу и тестировать его свойства. Если конкретный тип ActionResult может зависеть от разных входных данных, или контекста, то нам необходимо будет писать тесты для каждого сценария. Мы не будем приводить очень подробные примеры для всех конкретных типов ActionResult.

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

Введение в библиотеку для создания моков - Moq

Moq – известная библиотека с открытым исходным кодом, которая была разработана с использованием возможностей C# 3.0 (деревья выражений LINQ и лямбда-выражения). Одним из принципов библиотеки звучит как «с очень низким порогом входа получить хорошие возможности для рефакторинга». Moq достаточно проста в изучении и использовании и позволяет создавать моки на необходимые вам методы и свойства с использованием лямбда-выражений.

Загрузка и установка Moq

Moq хостится на Google Code. Вы можете скачать бинарники и документацию по ссылке http://code.google.com/p/moq/. После того, как вы загрузите архивный файл, вы увидите внутри несколько версий: для .NET 3.5, .NET 4.0, .NET 4.0 –NoCastle (Castle – это pen source проект для упрощения разработки .NET энтерпрайз и веб-приложений), а также версия для Silverlight 4.0. В нашем случае мы будем использовать версию для .NET 4.0.

Для того, чтобы использовать библиотеку в Visual Studio 2010, необходимо все лишь добавить ссылку на Moq.dll в проект и добавить пространство имен Moq в тестовые классы.

Что может быть реализовано как мок?

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

По первых, класс, который мы хотим мокнуть, не должен быть sealed.

Во-вторых, вы не сможете напрямую мокнуть статические методы (методы-расширения, по сути также являются статическими методами), так как Moq (как и другой инструмент для написания моков - Rhino Mocks) может лишь мокать экземпляры объектов. Одним из решений являтся создание небольшой обвертки вокруг этих методов, после чего мокать новосозданный класс. Этот шаблон имеет название адаптер. Подробнее об этом можно почитать на StackOverflow.

Давайте рассмотрим классы и методы, которые часто используются при написании моков.

Примечание. Как мокать методы-расширения.

Допустим, вы хотите использовать следующий метод-расширение:

someType.SendTo(address); 

Этот метод будет статическим и будет находиться в статическом классе:

public static class MessagingExtensions
{
    public static void SendTo(this SomeType target, string address)
    {
        // Do something
    }
}

Теперь, вместо такого объявления, что делает невозможным создания моков, вы должны определить статический метод – так называемую «точку входа» (entry point) для всех ваших методов-расширений, таким образом:

someType.Messaging().SendTo(address); 

Соответствующий класс MessagingExtensions будет теперь иметь такой вид:

public static class MessagingExtensions
{
    public static IMessaging Messaging(this SomeType target)
    {
        return MessagingFactory(target);
    }

    static MessagingExtensions()
    {
        MessagingFactory = st => new Messaging(st);
    }

    internal static Func MessagingFactory { get; set; }
}

Главная идея в том, что все статические методы (например, SendTo) будут находиться внутри интерфейса, которого легко можно мокнуть, после чего необходимо будет использовать фабрику для создания экземпляров этого интерфейса.

public interface IMessaging
{
    void SendTo(string address);
}

internal class Messaging : IMessaging
{
    SomeType someType;

    public Messaging(SomeType someType)
    {
        this.someType = someType;
    }

    public void SendTo(string address)
    {
        // Do something with someType and the address.
    }
}

Тест будет заменять фабрику и возвращать мок-объект:

var mockMessaging = new Mock();
MessagingExtensions.MessagingFactory = st => mockMessaging.Object;

Да, вам придётся написать немного больше кода, но так вы сможете легко мокать необходимые статические классы и методы.

Mock

С помощью этого класса мы можем получить объект типа Mock<T>, де T может обозначать интерфейсы или классы. Он имеет публичное виртуальное свойство Object, с помощью которого мы можем мокать необходимый объект. Давайте посмотрим на пример:

//define interface to be mocked
public interface IFake
{
  bool DoSomething(string actionname);
}

//define the test method
[TestMethod]
public void Test_Interface_IFake()
{
      //make a mock Object by Moq
      var mo = new Mock<IFake>();
      //Setup our mock object
mo
.Setup(foo => foo.DoSomething("Ping"))
.Returns(true);
//Assert it!
Assert.AreEqual(true, mo.Object.DoSomething("Ping"));
}

В приведенном примере мы мокаем интерфейс IFake путем создания экземпляра Mock, передавая IFake в качестве параметра. Потом мы вызвали метод Setup для инициализации объекта.

Обратите внимание, что аргумент метода Setup – это лямбда-выражение. Мы можем объяснить это таким образом: мок-объект foo вызывает свой метод DoSomething, который передает в качестве параметра строку 'Ping'.

И в конце мы используем Assert для проверки результата выполнения метода.

It

Это статический класс, который содержит набор статических общих методов: Is<TValue>, IsAny<TValue>, IsInRange<TValue> и IsRegex для фильтрации входящих параметров. Взгляните на следующий пример:

//define interface to be mocked
public interface IEmailSender 
{
   bool Send(string subject, string body, string email); 
}
//define the test method
[TestMethod]
public void User_Can_Send_Password()
{
   var emailMock = new Mock<IEmailSender>();
   emailMock
   .Setup(sender => sender.Send(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
   .Returns(true);
}

В этом примере показано, как мок-объект должен вызываться. Каждый раз, когда вызывается метод Send и в качестве параметров будут переданы строки (It.IsAny<string>(), то должно возвращаться значение true (Returns(true)).

Для более детальной информации по всем методам необходимо перейти на страницу с примерами Moq’s QuickStart.

Кроме того, расширенная версия класса It – это класс Match<T>, с помощью которого вы можете полностью настроить правила работы моков. Примеры вы найдете на выше упомянутой странице.

MockBehavior

Этот класс служит для конфигурирования поведения мок-объектов, например, стоит ли мокать объект в режиме по умолчанию. Давайте рассмотрим его объявление:

namespace Moq
{
    //Options to customize the behavior of the mock.
    public enum MockBehavior
    {
        //Causes the mock to always throw an exception for invocations that don't have
        //a corresponding setup.
        Strict = 0,
        //Will never throw exceptions, returning default values when necessary (null
        //for reference types, zero for value types or empty enumerables and arrays).
        Loose = 1,
        //Default mock behavior, which equals Moq.MockBehavior.Loose.
        Default = 1,
    }
}

Теперь, имея следующую ситуацию:

var mock = new Mock<IFake>(MockBehavior.Strict);

Здесь мы говорим Moq, что необходимо выбрасывать исключения для всех нереализованных объектов.

MockFactory

MockFactory – это фабрика мок-объектов, с помощью которой мы можем не только настраивать набор моков, но и тестировать их. Это отлично продемонстрировано в следующем примере:

var factory = new MockFactory(MockBehavior.Strict) { DefaultValue = DefaultValue.Mock };
// Create a mock using the factory settings
var fooMock = factory.Create<IFoo>();
// Create a mock overriding the factory settings
var barMock = factory.Create<IBar>(MockBehavior.Loose);
// Verify all verifiable expectations on all mocks created through the factory
factory.Verify();

Проверка

Иногда важно знать, какие методы были вызваны или часто сколько раз тот или иной метод был вызван. Для этого можно использовать метод Verify(). Давайте рассмотрим следующий пример:

// Method should be called only one time
mock.Verify(foo => foo.Execute("ping"), Times.Once());

Код пытается проверить метод Execute("ping"), который должен вызваться только единожды. Кроме Once, есть еще несколько доступных вариантов: AtLeast, AtLeastOnce, AtMost, AtMostOnce, Between, Equals, Exactly, Never.

После того, как мок готов, его поверка является достаточно простой задачей. Рассмотрим следующий пример:

//model to be tested
public interface IProductRepository
{
    IList<Product> FindAll();
    Product FindByName(string productName);
    Product FindById(int productId);
    bool Save(Product target);
}

public class Product
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public double Price { get; set; }
    public DateTime DateCreated { get; set; }
    public DateTime DateModified { get; set; }
}

//testing method
[TestMethod]
public void Test_FindByName_GetCalled()
{
    // create some mock data
    IList<Product> products = new List<Product>
        {
            new Product { ProductId = 1, Name = "C# Unleashed",
                Description = "Short description here", Price = 49.99 },
            new Product { ProductId = 2, Name = "ASP.Net Unleashed",
                Description = "Short description here", Price = 59.99 },
            new Product { ProductId = 3, Name = "Silverlight Unleashed",
                Description = "Short description here", Price = 29.99 }
        };
    Mock<IProductRepository> mock = new Mock<IProductRepository>();
    //mock
    //    .Setup(sender => sender.FindById(It.IsAny<int>()))
    //    .Returns((int s) => products.Where(
    //        x => x.ProductId == s).Single());
    mock.Object.FindById(1);
    mock
        .Verify(x => x.FindById(1), Times.Once());
}

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

Думаю, мы достаточно внимания отвели на изучение библиотеки Moq. В следующей статье рассмотрим основные принципы при тестировании контроллеров с помощью Moq и Ninject.

Ссылки по Moq:


Ищите нас в интернетах!

Комментарии

Свежие вакансии