Улучшите тестируемость, обернув статические классы

Некоторые части платформы .NET представлены как статические классы, например File, Directoryа также Path. Поскольку статические классы нельзя имитировать, очень сложно тестировать компоненты, которые их используют. Кроме того, некоторые статические классы могут иметь внешние побочные эффекты или зависимости, например File.ReadAllText требует, чтобы фактический физический файл находился в указанном месте, иначе вызов завершится ошибкой. Это усложняет наш тестовый код, требуя от нас создания файла в известном месте для теста, а затем его очистки. Это также нарушает правило «Тестирование в изоляции», поскольку теперь тест зависит от фактической базовой файловой системы.

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

Проект размещен в Репозиторий GitHub содержащий две ветви:

  • трудно тестируемый: Эта ветвь напрямую использует System.Io.File для чтения файла и иллюстрирует пару распространенных проблем при тестировании кода, использующего статические классы.
  • с фреймворками-обертками: эта ветвь вводит простую оболочку для инкапсуляции вызовов System.Io.File и улучшить тестируемость

Решение представляет собой простой пример, состоящий из двух проектов:

  • Tdd.FrameworkWrappers.Lib: Тестируемая реализация. Он содержит один класс, FileReaderкоторый читает файл из хранилища и возвращает его содержимое в виде строки.
  • Tdd.FrameworkWrappers.Lib.Tests: тесты NUnit для Tdd.FrameworkWrappers.Lib проект.

Начнем с клонирования difficult-to-test ветвь и открытие решения.

Если мы посмотрим на FileReader.cs, мы увидим, что его ReadText метод просто делегирует System.Io.File.ReadAllText и возвращает свой результат:

FileReader.cs

Однако все становится более запутанным, когда мы копаемся в FileReaderTests.cs. Прямое использование System.Io.File несколько усложняет наше тестирование:

  • Мы не можем проверить взаимодействие между FileReader а также System.Io.File потому что статические классы нельзя издеваться
  • Нам нужно добавить методы до и после тестирования для создания и очистки тестового файла в системе. Это само по себе может быть проблематичным, поскольку чтение/запись/удаление файлов может быть подвержено ошибкам (т. е. IoExceptions), поэтому наш тест может предположительно провалиться по причинам, выходящим за рамки теста.

FileReaderTests.cs

Мы можем решить эту проблему тестируемости, обернув System.Io.File в другом нестатическом классе, который мы контролируем. Тем самым мы можем раскрыть IFile интерфейс (чтобы мы могли имитировать и поддерживать внедрение зависимостей). IFile интерфейс реализован FileImplкоторый будет конкретным типом, внедренным в экземпляры FileReader нашим контейнером IoC.

Итак, без лишних слов, давайте копать!

Теперь давайте клонируем with-framework-wrappers ветку и откройте решение.

Мы начнем с того же места, что и раньше, в FileReader.cs. Обратите внимание, что теперь есть конструктор, который принимает IFile а также ILogger экземпляр, и что ReadText метод теперь вызывает IFile.ReadAllText вместо File.ReadAllText.

FileReader.cs

Вы также заметите, что появился новый FrameworkWrappers папка, так что давайте копаться в этом. FrameworkWrappers содержит IFile и его реализация, FileImpl. Глядя на интерфейс, мы видим, что он выставляет ReadAllText метод, сигнатура которого совпадает с сигнатурой System.Io.File.ReadAllText:

IFile.cs

Переходим к FileImplмы видим, что он делает именно то, что FileReader делал раньше: просто делегировал вызов System.Io.File.ReadAllText и возвращая результат:

FileImpl.cs

Если мы теперь перейдем к тестам, то увидим еще некоторые изменения, а именно:

  • CreateTestFile а также CleanupTestFile методы исчезли, потому что нам больше не нужно писать в файловую систему, чтобы выполнить этот тест (например, мы удалили зависимость теста от базовой файловой системы)
  • SetUp был изменен для создания экземпляра Mock<IFile> а также Mock<ILogger> и вводить их в FileReader под тестом
  • ReadText_WhenFileExists_ReturnsFileContents существенно упрощен
  • ReadText_Always_PerformsExpectedWork теперь можно реализовать — поскольку мы используем макет, мы можем убедиться, что ReadText метод действительно вызывает ReadAllText метод на IFile с ожидаемыми аргументами.
  • Теперь мы также можем тестировать «грустные» потоки, например, «Делает ли FileReader реагировать соответствующим образом, если исключение выдается IFileReader.ReadAllTextReadText_WhenIoExceptionThrown_PerformsExpectedWork иллюстрирует эту новую возможность
  • Тестовые случаи параметризуются с помощью TestCaseSourceAttribute. На самом деле это не относится к тестированию с помощью Framework Wrappers, но это просто способ повторного использования тестовых случаев.

FileReaderTests.cs

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

Итак, в двух словах, то, что мы сделали здесь, это улучшили тестируемость, добавив тонкий слой косвенности вокруг System.Io.File class, что позволяет нам ввести интерфейс (для DI и Mocking), реализованный контролируемым нами классом. Конечным результатом является то, что мы можем эффективно тестировать наши FileReader класс в полной изоляции от внешних зависимостей.

Я склонен следовать этому шаблону почти каждый раз, когда мне нужно использовать статический класс, например File, Directory, Path, ConfigurationManagerи т. д. Я также использовал вариант этого шаблона с приложениями WPF, заключая компоненты пользовательского интерфейса, не поддерживающие WPF, в прикрепленное поведение, чтобы облегчить связь между ViewModels и устаревшими компонентами пользовательского интерфейса.

Застрявший? Хотите больше информации?

Свяжитесь со мной на CodeMentor для живого руководства 1:1!

Получить помощь по Codementor

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *