Feel Good.

10 марта 2010

Hello, Mock

Во время разработки(тестирования) иногда возникает потребность использовать некий интерфейс, до того, как он будет реализован. Решением данной проблемы является использование Mock-объектов.

Mock-объект предоставляет фиктивную реализацию, другими словами, Mock-объект лишь имитирует поведение. Приведу пару ситуаций, когда имитирование поведения будет кстати:
  1. Параллельная разработка, заглушка.

    Представьте ситуацию, вы работаете в паре параллельно над одной задачей, причем Ваша задача напрямую зависит от задачи напарника. Например, Ваш напарник реализует Repository-класс для предоставления доступа к данным, в то время как Вы реализуете Service-класс, содержащий основную логику приложения и использующий данный Repository-класс. Оптимальное решение - реализовать Mock-repository, и отлаживать свой код не дожидаясь напарника.

  2. Тестирование.

    Часто бывает, что во время тестирования возникает проблема воспроизведения конкретной тестовой ситуации: Вы тестируете модуль, который зависит от другого стороннего модуля, повлиять на работу которого Вы никак не можете, например, Ваше приложение и сторонний WCF-сервис. И в этом случае, имитацию WCF-прокси может произвести наш Mock-объект.

Вы спросите: "Как же получить этот Mock-объект? И как он тогда наделяется поведением?". Отвечу, что все дело в Mock-фреймворке. Как следует из названия, именно он выполняет роль контейнера для Mock-объектов. На текущий момент существует длинный список Mock-фреймвоков: Moq, NMock, Rhino Mocks и другие, но в этой статье в качестве примера рассмотрим только один из них Moq. Следует отметить, что Moq адаптирован под .NET 3.5, поддерживает лямбда выражения и является строго типизированным. Эти особенности делают его простым, продуктивным и удобным в использовании. Помимо этого, Moq позволяет имитировать работу методов(methods), свойств(properties), событий(events). Поддерживает out/ref параметры, валидацию процесса имитирования Mock-объекта и управление поведением(Mock Behavior).

Для того чтобы начать работу необходимо скачать и добавить ссылку в проекте на Moq.dll. В качестве простого(подробно здесь) примера создадим Mock-объект для следующего интерфейса:


public interface IPing

{

    bool Ping(string host);

}


Со следующим тестовым поведением: метод Ping возвращает true только для google.com без учета регистра, для остальных доменов-false, но при пустом аргументе метод должен генерировать исключение. Видно, что метод Ping описывает простейшую модель типа вход(имя хоста)-обработка(отправка/получение ICMP Echo Request/Reply запроса/ответа)-выход(результат, прошел ли запрос или нет). Поэтому, для таких типов моделей, у которых известен вход и выход, мы можем просто выполнить имитацию процесса обработки, при этом потратив минимум времени, не усложняя код:

// Mock-фреймворк для интерфейса IPing

Mock<IPing> mock = new Mock<IPing>();           

 

// Настроим IPing.Ping(host)

mock

    .Setup

    (

        mockPing => mockPing

            .Ping

            (

                It.Is<string>

                (

                    host => host

                        .ToLower()

                        .Equals("google.com")

                )

            )

    )

    .Returns(true);

 

// При пустом аргументе выбрасываем исключение.

mock

    .Setup

    (

        mockPing => mockPing.Ping(string.Empty)

    )

    .Throws<ArgumentNullException>();

 

// ping - mock объект.

// Далее используем его, как искомый объект.

IPing ping = mock.Object;


Стоит отметить, что большинство Mock-фреймворков (в том числе и Moq), являются контейнерами для Mock-объектов, что позволяет им следить за всеми вызовами/обращениями к методам/свойствам соответствующего Mock-объекта и с какими параметрами. К примеру:


// Если метод Ping ни разу не вызывался,

// здесь будет выброшено исключение MockException

mock.Verify(mockPing => mockPing.Ping(It.IsAny<string>()), "Метод Ping не вызывался.");

 

// Если метод Ping вызывался хоть раз с пустым параметром и

// исключение ArgumentNullException было обработано,

// здесь будет выброшено исключение MockException

mock.Verify(mockPing => mockPing.Ping(string.Empty), Times.Never());


Многие могут заметить, что в методе Setup получось много кода, который можно было бы вынести, для этого Moq позволяет создать custom-предикат:


mock

    .Setup

    (

        mockPing => mockPing.Ping(IsGoogle())

    )

    .Returns(true);


// Предикат.

static string IsGoogle()

{

    return Match<string>

        .Create

        (

            host => host

                .ToLower()

                .Equals("google.com")

        );

}


Вывод: если рассматривается модель типа вход-обработка-выход, у которой известен вход, а так же выход(реакция на вход) системы, то использование Mock-объектов будет оптимальным решением, а задание поведения Mock-объекта сведется к простому сопоставлению входа и выхода системы без применения сложной логики. Следует помнить тривиальное правило, что усилия, потраченные на реализацию Mock-функционала должны быть намного меньше, чем усилия, потраченные на реализацию искомого функционала. Mock-решение - простое, быстрое и временное решение.

8 комментариев:

  1. Как раз недавно изучил Moq.
    Очень уж простое описание. Примеров бы побольше. Но за труды все равно спасибо.

    ОтветитьУдалить
  2. @M
    Спасибо за отзыв. Но я стремился передать читателю что такое Mocking и главное, где это можно применять, а чтобы не быть слишком абстрактным привел простой пример на Moq.

    ОтветитьУдалить
  3. Насколько я понял цель статьи не подбор примеров, а скорее описание проблемы, решение которой подводит нас к использованию Mocking framework'а.

    Было бы неплохо привести еще подборку хороших ссылок по этому фреймверку, чтобы читатель мог дальше знакомиться с subj.

    Если будет желание, то я периодически сбрасывал ссылки с комментариями касательно mocking'а в свой твиттер: http://twitter.com/alexey_diyan

    На всякий случай отмечу, что я предпочитаю все же Rhino.Mock (несмотря на его перегруженный API), поэтому мои твитты нельзя считать нейтральными :)

    ОтветитьУдалить
  4. @All
    Добавил простой пример, демонстрирующий работу Verify-методов Moq.

    Добавил простой пример, демонстрирующий работу Match-методов Moq.

    Вообще, идея работы с Mock простая, мы описываем множество входных параметров (Moq.It.*) которому сопоставляем множество выходов (Moq.Language.IReturns).

    ОтветитьУдалить
  5. Доброго дня! А можно выложить проект с примером Moq

    ОтветитьУдалить
  6. Спасибо за ссылку пример.
    Mock - объекты всегда используются для поведенческого тестирования. Для простых unit тестов они применимы?

    ОтветитьУдалить
  7. Да, например, мы хотим протестировать парсер SomeParser, который имеет зависимость на объект типа IPacket, и чтобы не имплементировать IPacket мы построим для него mock-объект:

    Mock<IPacket> packet= ...;

    IParser parser = new SomeParser();

    var expected = ....
    var actual = parser.Parse(packet.Object);

    Assert.Equals(expected, actual)

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