Страницы

вторник, 13 марта 2012 г.

Эффективное тестирование. Боремся с зависимостями.

  Рано или поздно в процессе тестирование мы натыкаемся на зависимости, которые мешают протестировать систему в изолированном состоянии, или просто делают это невозможным (попробуйте сделать MessageBox.Show() в неинтерактивном режиме). В таких случаях нам надо как-то отделить нетестопригодные зависимости. Для этих случаев нам надо некие заглушки для тестов. Как быть в этой ситуации, рассмотрим на примере. Пусть у нас есть класс, который мы собираемся протестировать:

public class Order
{
    //skipped
}
public interface IOrderListView
{
    IEnumerable<Order> Orders { get; set; }
}
    
public class OrderListPresenter
{
    private readonly ISession _session;
    private readonly IOrderListView _view;
    public OrderListPresenter(ISession session, IOrderListView view)
    {
        _session = session;
        _view = view;
    }
    public void Start()
    {
        _view.Orders = _session.QueryOver<Order>().Take(100).List<Order>();
    }
}

  Мы хотим протестировать простую вещь: после вызова метода Start() в IOrderView передан Order  и свойство Visible == true. В коде две зависимости: IOrderListPresenter и ISession. В былые времена единственным способом протестировать OrderListPresenter было использование рукописных реализаций интерфейсов (и сейчас много случаев, когда стоит воспользоваться такой возможностью, но об этом в следующих сообщениях). К счастью, научно-технический прогресс уже решил эту проблему: в мире тестов хозяйствуют Mock-объекты.  А сейчас посмотрим на модные гаджеты для создания Mock-объектов на лету.


Moq


  В текущий момент пожалуй наиболее популярным фреймворк. Хотя его разработка находится в анабиозном состоянии. Гугл по по привычке ведёт на старый официальный сайт, хотя проект некоторое время назад переехал на codeplex (хотя процесс переезда еще не завершён, и содержание codeplex’а весьма куцее). Итак, наше тест с Moq  будет выглядеть так:

[Test] 
public void On_start_puts_in_view_some_orders_behaviour_style()
{
    //Создаём двойник интерфейса IOrderView
    var viewMock = new Mock<IOrderView>();
    var order = new Order(10);
    //Создаём двойник интерфейса ISession
    var sessionMock = new Mock<ISession>();
    sessionMock
        //При вызове метода Get<Order> с аргументов 10
        .Setup(x => x.Get<Order>(It.Is<long>(id => id.Equals(10))))
        //И вернём 
        .Returns(order);
    var presenter = new OrderPresenter(sessionMock.Object, viewMock.Object);
    presenter.Start(10);
    viewMock.VerifySet(x => x.Visible = true);
    viewMock.VerifySet(x => x.Order = order);
}

  Как можно догадаться, в последних двух строчках мы проверяем ожидания вызовов методов. Но я люто ненавижу код, где мне приходится выполнять такие проверки. Проверка вызовов методов приоткрывает чёрный ящик реализации класса, тем самым жёстко привязывая к ней тест. Любое изменение в коде класса тут же приводит к необходимости менять код ожиданий. Надо ли объяснять, что это называется связанность (coupling), и вообще плохо?  На мой вкус, этот тест будет гораздо проще, если переписать его в стиле state-driven тестирование:

[Test] 
public void On_start_puts_in_view_some_orders_state_style()
{
    var viewMock = new Mock<IOrderView>();
    //Устанавливаем поведение всех свойств по-умолчанию. 
    viewMock.SetupAllProperties();
    var order = new Order(10);
    var sessionMock = new Mock<ISession>();
    sessionMock
        //При вызове метода Get<Order> с аргументов 10
        .Setup(x => x.Get<Order>(It.Is<long>(id => id.Equals(10))))
        //И вернём 
        .Returns(order);
    var presenter = new OrderPresenter(sessionMock.Object, viewMock.Object);
    presenter.Start(10);
    viewMock.Object.Satisfy(view =>
                            view.Visible == true &&
                            view.Order == order);
}

  Полностью чёрный ящик всё равно не получится, но степень информированности о внутренностях класса уменьшилась. Этот вопрос хорошо расписал Фаулер, рекомендую к прочтению.


  Стоит отметить, что Moq умеет подделывать не только интерфейсы, но и non-sealed классы и даже переопределять виртуальные методы. Эти ограничения вытекают из Castle.DynamicProxy – подделки генерируются на лету в динамическую сборку, и на них распространяются все ограничения C#.


  Впрочем, я бы отметил два существенных недостатка Moq:



  • Полузаброшенное состоянии. Это не так страшно само по себе, но наличие неприятного бага может существенно осложнить жизнь.
  • Для динамической генерации двойников Moq использует интегрированный Castle.DynamicProxy. Это удобно, но может приводить к конфликту имён.

  Тем не менее – это взрослый фреймворк, готовый разрешить почти все необходимые случаи. Из альтернатив стоит отметить FakeItEasy, но на мой взгляд его API не самое удобное, и не позволяет тестировать вызовы свойств. В общем, Moq всё же верней. Есть еще Rhino.Mocks, но его API пресыщено наследием ранних версий, сейчас выглядит неуклюжим. Аyende Rahien похоже более не собирается его поддерживать, несмотря на заявленные планы по выпуску версии 4.0.


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

1 комментарий:

  1. Вместо
    .Setup(x => x.Get(It.Is(id => id.Equals(10))))
    можно обойтись просто .Setup(x => x.Get(10))

    ОтветитьУдалить