Еще один шаблон?
Ну да. Я пишу модульные и интеграционные тесты почти каждый день, и по ходу дела я научился различным трюкам и хитростям о том, как повысить производительность и как писать менее хрупкие тесты.
Но один из появившихся шаблонов я никогда не видел в коде других людей, поэтому решил поделиться им здесь, так как считаю его очень полезным.
Я называю это « Шаблон контекста тестирования» и, в отличие от его названия, очень прост.
Идея состоит в том, чтобы создать в своем тестовом приборе закрытый класс с именем (как вы уже догадались) 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, показывающее это в действии, если вы заинтересованы.
Я надеюсь, что этот простой шаблон может помочь кому-то в написании лучших тестов.
Если у вас есть комментарии или если вы делаете что-то подобное в своих тестах, оставьте комментарий, и я хотел бы услышать его.