Статьи

Представляем шаблон контекста модульного тестирования

Еще один шаблон?

Ну да. Я пишу модульные и интеграционные тесты почти каждый день, и по ходу дела я научился различным трюкам и хитростям о том, как повысить производительность и как писать менее хрупкие тесты.

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

Я называю это « Шаблон контекста тестирования» и, в отличие от его названия, очень прост.

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

Таким образом, у нас есть все необходимое для тестирования в одном месте, и мы можем начать веселиться!

Так что, если мы тестируем класс WebPageDownloader, которому для работы нужны еще две сущности ( IUrlPermissioner и IUrlRetriever ), то мы создаем класс TestContext следующим образом:

private class TestContext
{
    public Mock<IUrlPermissioner> PermissionerMock;
    public Mock<IUrlRetriever> UrlRetrieverMock;
    public WebPageDownloader Downloader;
}

Обратите внимание, что здесь мы представляем макеты зависимостей от контекста (я использую среду тестирования Moq, но вы можете выбрать любой другой, который вам нравится), это очень важно, потому что позже в наших тестах мы можем дать дополнительные настройки / ожидания для этих макетов. которые используются внутри тестового класса.

Затем мы создаем приватный метод на тестовом приспособлении, который создает для нас экземпляр контекста, инициализирует его всеми макетами, необходимыми для тестирования, и самим экземпляром тестируемого класса и возвращает его (чтобы избежать создания этого в каждом тесте):

private TestContext CreateContext()
{
    var ctxt = new TestContext()
    {
        PermissionerMock = new Mock<IUrlPermissioner>(),
        UrlRetrieverMock = new Mock<IUrlRetriever>(),
    };

    ctxt.Downloader = new WebPageDownloader(ctxt.PermissionerMock.Object, ctxt.UrlRetrieverMock.Object);

    return ctxt;
}

Как вы видите, я создаю все макеты, в которых нуждается наш протестированный класс, затем создаю его экземпляр, передавая макеты в его конструктор, а затем сохраняю макеты и целевой класс в контексте для использования в дальнейших тестах.

Тестирование контекста в действии

Поэтому теперь в каждом тесте я вызываю метод CreateContext, чтобы получить свежий экземпляр TestContext и начать тестирование моего целевого класса.
Хорошая вещь об этом в том, что в TestContext у нас есть все макеты зависимостей, используемые для создания тестируемого класса, так что мы все еще можем манипулировать ими, макетировать некоторые конкретные методы для каждого теста, а также мы можем проверить позже в тесте, если mocks были использованы правильно, проверьте, какие методы вызывал для них целевой класс и т. д.

Вот пример теста, который проверяет, генерирует ли наш класс исключение для URl, которое не разрешено IUrlPermissioner:

[Test]
[ExpectedException(typeof(SecurityException))]
public void Download_WhenNotPermitted_Throws()
{
    var ctxt = CreateContext();

    var badUrl = "http://www.SomeBadUrl";

    // arrange
    ctxt.PermissionerMock.Setup(a => a.IsUrlAllowed(badUrl)).Returns(false);

    // act
    ctxt.Downloader.Download(badUrl);

    // assert we don't need to do since we have ExpectedException attribute
}

Как вы можете видеть в тесте, мы просто вызываем метод CreateContext для получения нового контекста, затем мы используем макеты из контекста для настройки наших ожидаемых вызовов и возвращаемых значений, а затем мы вызываем фактический метод для класса, который мы тестируем, и затем ожидать исключения (через атрибут ExpectedException, который мы поместили в метод теста)

Вот немного более сложный сценарий, где мы устанавливаем несколько ложных взаимодействий с нашим классом, а затем ожидаем, что возвращаемый результат будет правильным:

[Test]
public void Download_WhenAllowed_ShouldReturnWebpageGotViaRetriever()
{
    var ctxt = CreateContext();

    var OkUrl = "http://www.SomeOkUrl";
    var OkUrlContent = "Some Web Page";

    // arrange
    ctxt.PermissionerMock.Setup(a => a.IsUrlAllowed(OkUrl)).Returns(true);
    ctxt.UrlRetrieverMock.Setup(a => a.Retrieve(OkUrl)).Returns(OkUrlContent);

    // act
    var result = ctxt.Downloader.Download(OkUrl);

    // assert
    Assert.AreEqual(OkUrlContent, result);
}

Еще одна полезная вещь: если вы позже выполните рефакторинг своего целевого класса и добавите / удалите зависимости, вы можете легко изменить методы TestContext и CreateContext соответствующим образом, и ни один из ваших существующих тестов не должен скомпилироваться из-за этого, в отличие от ситуации, когда вы все это делаете в каждом тесте.

Как обычно, вы можете скачать решение Visual Studio 2012, показывающее это в действии, если вы заинтересованы.

Я надеюсь, что этот простой шаблон может помочь кому-то в написании лучших тестов.
Если у вас есть комментарии или если вы делаете что-то подобное в своих тестах, оставьте комментарий, и я хотел бы услышать его.