Титры к видео уроку - "Использование Stub объектов для Unit тестов"

Ссылка на полный видео урок

Здравствуйте. Мы переходим ко второму уроку разработки через тестирование. В этом уроке мы разберём, что такое внешняя зависимость, мы разберём, какие недостатки есть у внешней зависимости, и как от неё избавиться. Также мы рассмотрим, что такое Stub объекты, зачем они нужны и как их непосредственно использовать. Также мы будем говорить о Dependency Injection контейнерах и поговорим о проблемах инкапсуляции при создании удобного кода, удобного дизайна для тестирования.

Но, сперва, мы с вами рассмотрим, что такое внешняя зависимость. Внешней зависимостью мы будем называть неконтролируемый объект нашей системы, с которым взаимодействует тестируемый код. Внешней зависимостью может быть, например, файловая система, потоки, память, службы и т.д.

И в этом уроке мы будем рассматривай объект FileManager, который позволяет нам работать с файловой системой. И, вот тут у нас появляется проблема. Внешняя зависимость – это FileSystem – файловая система для нашего объекта FileManager. И дело в том, что у нас просто-напросто не получится создать модульные тесты для тестирования нашего класса FileManager, потому что, как бы мы не старались – это будет интеграционный тест, потому как мы будем тестировать не только FileManager, но и файловою систему, то есть: наличие файла, правильность формат файла, правильность разрешения и т.д. И для того, чтобы избавится от этой внешней зависимости мы с вами произведём несколько шагов: первое что мы сделаем – это те методы, которые работают с файловой системой вынесем в отдельный объект FileDataObject и наш объект FileManager будет работать не непосредственно с нашей файловой системой, а через объект FileDataObject. Этот объект мы с вами будем называть косвенным слоем и мы его создали для избежания прямой зависимости от файловой системы. В дальнейшем мы сделаем так, чтобы объект FileDataObject можно было заменить неким фейковым объектом, то есть некой заменой, который мы будем называть Stub объектом.

Для этого нам потребуется в объекте FileDataObject определить интерфейс, с которым взаимодействует объект FileManager. Мы оделим этот интерфейс и назовём его IDataAcessObject. И вот теперь у нас будет возможность заменить реальный объект формальным. FileDataObject, который реально работает с некой файловой системой – фейковым объектом, который на самом деле не работает с файловой системой, но возвращает те значения, которые мы хотим, чтобы он возвращал. Для этого нам всего лишь придётся создать класс, который мы назовём StubFileDataObject и этим классом реализовывать интерфейс IDataAcesssObject. И вот тут уже можно считать, что мы с вами познакомились с тем, что такое Stub объект, то есть – управляемая замена существующей зависимости в нашей системе. Stub объекты позволяют тестировать код без использования внешних зависимостей.

Теперь давайте перейдём к следующему слайду, где будет показано, в каких случаях, как правило, будет появляться внешняя зависимость. Она будет появляться в следующих ситуациях: при создании объекта явно указывается класс – будет приводить к создание внешней зависимости. Также зависимость от аппаратных программ и платформ, зависимость от представления и реализации и зависимость от алгоритма. Сильная связанность будем порождать следующие проблемы: систему будет сложно поддерживать, расширять, понимать и тестировать. Для того, чтобы уходить от внешней зависимости мы с вами будем придерживаться некого принципа Inversion of Control. Это абстрактный принцип, который описывает методы написания слабосвязанного кода. И один из способ реализации этого принципа – это Pattern Dependency Injection. Это паттерн, который описывает технику внедрения внешней зависимости программному компоненту. Этот паттерн имеет множество преимуществ, среди которых: разделение конфигурирования и использования объектов, уменьшение связи между объектами, конкретные объекты проще заменять, увеличение мобильности модулей и также при внедрении этого патер на систему будет проще сопровождать и тестировать. Существуйте несколько способов внедрения зависимости: это внедрение через конструктор, внедрение через свойство и внедрение через интерфейсы. И для того, чтобы удобно создавать экземпляры зависимостей мы с вами будем пользоваться Dependency Injection контейнерами. Он представляет из себя набор объектов, который позволяет упростить и автоматизировать процесс написания кода с использованием принципа Inversion of Control. Далее нам представлены различные техники внедрения зависимостей. Первое что мы видим это техника внедрения зависимостей через конструктор. Мы будем с вами работать с тем же объектом FileManager, о котором мы с вами говорили до этого. В классе файл менеджер мы создадим конструктор, который будет принимать значение IDataAccessObject. Во время создания тестов для нашего класса, в котором есть внедрение зависимости через конструктор, у нас будет возможность заменить реальный объект, который реализует интерфейс IDataAccessObject и который работает с файловой системой, на Stub объект, который, на самом деле, имеет те же методы, но на самом деле – фиктивно. То есть на самом деле этот объект не работает напрямую с файловой системой, а просто возвращает те значения, которые нам нужны. Ниже мы видим с вами вызов Stab объекта. Мы можем обратиться на том объекте, который мы передали в качестве параметра конструктора, и вызвать на нём метод GetFiles. Что он будет делать – уже зависит от конкретной реализации.

Также возможно внедрение зависимости через свойство. В данном примере мы создали свойство с именем dataAccessObject типа iDataAccessObject в теле нашего класса FileManager и реализовали в нём метод set и метод get. В методе set мы будем полю dataAccesObject объекта FileManager мы присваиваем то значение, которое мы передаём этому свойству. Метод get будет работать следующим образом: он будет проверять, если у нас dataAccessObject – null, значит мы будем генерировать исключительную ситуацию типа MemberAccessException с сообщением “DataAccesObject has not been initialized”, то есть мы не инициализировали это поле. Если поле dataAccessObject будет иметь значение не null – значит мы, просто-напросто будем возвращать значение поля из нашего конструктора. И вызов метода на Stab объекте у нас будет иметь абсолютно такой же вид, правда мы будем обращаться к Stab объекту через свойство, а не через поле.

Также возможно внедрение зависимости через интерфейс. В данном случае объект FileManager не будет хранить поле, в котором будет храниться экземпляр класса, реализующего интерфейс IDataAccessObject. При вызове файловой системы тут же будет внедряться зависимость и в теле метода будет вызываться вызов метода Stab объекта, который мы туда передаём.

В первом примере мы рассмотрим проблему тестирования класса с внешней зависимостью. Класс, который мы будем тестировать, называется FileManager. В нашем файле есть всего лишь один член – это метод с именем FindLogFile, в котором мы передаём строковой литерал, который будет говорить о том, какой файл мы хотим проверить. В теле нашего метода на 14 строке создаётся экземпляр класса FileDataObject, то есть объекта, который будет напрямую работать с файловой системой. В этом объекте у нас есть всего один метод, который возвращает экземпляр коллекции list с закрытым типом string. В теле метода GetFiles на 14 строке мы получаем имя – путь к текущей директории проекта, на 16 строке создаем коллекцию элементов list типа string, на 18 строке мы получаем информацию по директории по пути, который мы получили на 14 строке. На 20 строке на объекте DirectoryInfo, который мы получили, вызывая метод GetFiles, получаем массив элементов FileInfo, каждый из которых имеет информацию о том файле, который находится в нашей директории. На 22 строке с помощью цикла for-each мы перебираем все элементы массива Files и добавляем каждый из этих элементов в коллекцию List, которую мы создавали на 16 строке. И, в конце концов, после окончания работы нашего метода, будет возвращаться ссылка на коллекцию list, которую мы создали и заполнили в теле этого метода. Теперь давайте вернёмся к нашему объекту FileManager. После создания объекта FileDataObject в теле метода FindLogFile, на 17 строке на объекте FileDataObject мы вызываем метод GetFiles и получаем коллекцию типа list элементов типа string. На 19 строке перебираем все элементы нашей коллекции, и на 21 строке проверяем каждый из элементов коллекции, содержит ли он ту строку, которую мы передаём в качестве параметра метода FindLogFile, тогда мы возвращаем true, иначе метод будет возвращать значение false. И также мы вами создали тестовый метод, который позволяет нам протестировать работу метода FindLogFile объекта FileManager. Мы создали тестовый метод FindLogFileTest1. На 16 строке в теле тестового метода мы создаём экземпляр класса FileManager, на 17 строке мы вызываем на объекте FileManager метод FindLogFile и передаём туда имя файла, который хотим найти. Мы хотим найти файл с именем “File2.log”. И давайте запустим наш тест. Visual Studio сейчас будет сканировать те тесты, который есть в нашем солюшине, а пока что давайте поговорим о недостатках, которые мы тут имеем. Мы видим, что объект FileManager тесно связан с объектом FileDataObject, тем самым напрямую зависит от файловой системы и протестировать такой класс будет очень сложно, так как нам придётся иметь в директории нашего проекта файл с именем “File2.log”. Только в таком случае мы сможем посмотреть, правильно ли работает наш метод или же неправильно. И неплохо было бы вот это слой объектов FileManager, FileDataObject заменить на какой-то тестовый объект. И на 15 строке и нас как раз закомментировано создание объекта TestDataObject, который напрямую не будет иметь дело с файловой системой. Этот метод, который имеет в своем теле метод GetFiles, который возвращает экземпляр коллекции list – закрытый типа string. Но на самом деле на 13 строке в теле этого метода будем создавать экземпляр коллецкии list, добавлять в коллекцию три элемента: “file1.txt”, “file2.log”, “file3.exe”, и возвращать ссылу на эту коллекцию. Давайте перейдём к объекту FileManager и закомментируем 14 строку и раскомментируем 15 строку. Давайте попробуем запустить наш тест. Наш тест тест имеет имя FindLogFileTest1, но я не могу найти его в Test Explorer-e, поэтому давайте нажмём Run Tests. Вот появился этот тест и выполнился у нас успешно. Если бы мы создавали всё таки объект FileDataObject, то тест конечно же будет провален, потому как в директории нашего проекта не существует файла “file2.log”. Или он существует. Ну давайте это проверим. Да, на самом деле такой файл есть, но, тем не менее, мы всё равно имеем тут неудобство тестирования нашего класса. В следующем примере мы с вами попробуем использовать паттерн DependencyInjection для того, чтобы ослабить связь между объектом FileManager и файловой системой.

Во втором примере мы изменили наш дизайн таким образом, что объект FileManager не зависил от конкретного объекта, который получает доступ к файловой системе. Мы просто-напросто в объекте FileManager определили интерфейс, через который объект должен общаться с файловой системе. Этот интерфейс мы назвали IDataAccessObject, в котором создали неявно-абстрактный один метод с именем GetFiles, который ничего не принимает, но возвращает коллекцию элементов типа string. Для того, чтобы внедрить зависимость класса FileManager от файловой системы, мы воспользовались техникой внедрения через конструктор. У нас в объекте FileManager есть два конструктора: один конструктор у нас “по умолчанию”, который инициализирует полe DataAccessObject экземпляром класса FileDataObject; второй конструктор у нас пользовательский, который инициализирует поле DataAccessObject тем параметром, который мы передаём в наш конструктор. При этом у нас появляется возможность заменить конкретную реализацию, которая работает у нас с файловой системой неким Stab объектом, который просто-напросто возвращает ожидаемые результаты, которые мы хотим. На 19 строке мы видим метод FindLogFile объекта FileManager. Этот метод, как и в предыдущем примере принимает имя файла, который мы хотим проверить, и вызывает метод GetFiles на поле DataAccessObject – у нас возвращается коллекция с элементов типа list с элементами типа string, и дальше в цикле for-each мы проверяем, если один из элементов содержит подстроку, который мы передаём в качестве параметра методу FindLogFile, то метод возвращает значение true, иначе – false. Давайте посмотрим на тело нашего тестового метода, как мы будет писать тесты для нашей систем. Мы хотим протестировать метод FindLogFile объекта FileManager. На 16 строке в теле тестового метода FindLogFileTest2 мы создали экземпляр класса FileManager, воспользовались пользовательским конструктором, который принимает значение типа интерфейса iDataAccessObject. Для того чтобы избавиться от зависимости с файловой системой мы создали объект StubFileDataObject. Это стабовый объект для нашего интерфейса. Это класс, который реализует интерфейс iDataAccessObject. И в теле метода GetFiles просто напросто создаётся экземпляр коллекции list, который заполняется некими значениями. Из нашего метода как раз будет возвращаться ссылка на эту коллекцию. Тем самым наш подставной объект StubFileDataObject на самом деле не работает с файловой системой. Когда мы будем запускать наш тест – он будет успешно проходить, потому как действительно в той коллекции, которую возвращает наш подставной объект будет файл с именем “file2.log”. Давайте запустим этот тест. Это FindLogFileTest2. Мы видим, что есть ещё один тест с таким именем, но в любом случае все тесты у нас прошли, и это говорит о том, что наша техника внедрения зависимости через конструктор действительно успешно сработала. Если мы хотим протестировать класс, который будет обращаться к файловой системе, то мы можем воспользоваться конструктором по умолчанию, но не факт, что наш тест будет успешно завершён, потому как файла “file2.log” в директории нашего проекта может не оказаться, и это действительно так. Внедрение через конструктор проблемно использовать, если для правильной работы тестируемого класса требуется несколько стаб объектов. В таком случае приходится создавать или множество конструктов, или один конструктор с множественным параметром. В следующем примере мы с вами посмотрим способ внедрения через свойство, который используется как раз если зависимость имеет опциональный характер.

Третий пример нам продемонстрирует технику внедрения зависимости через свойство. Мы изменили класс FileManager: мы избавились от конструктора, который был у нас в предыдущем примере, и для того чтобы внедрить зависимость в класс FileManager мы создали открытое свойство dataAccesObject типа iDataAccessObject для того, чтобы получать доступ для чтения и записи к закрытому полю dataAccessObject. При этом метод set присваивает значение полю dataAccessObject, медот get проверяет, если поле dataAccessObject не инициализировано, значит мы будем генерировать исключительную ситуацию MemberAccesException. 27 строка: если поле у нас не null, значит мы будем возвращать значение поля dataAccessObject. 31 строка: метод FindLogFile объекта FileManager у нас ничем не изменился, кроме того, что для того, чтобы вызвать метод GetFiles мы используем уже не поле, а свойство. Это нужно для того, чтобы перед тем, как получить свойство, проверить инициализировано ли поле dataAccessObject. Давайте перейдём в метод теста. В методе FindLogFileTest3 на 16 строке мы создали экземпляр класса FileManager, на 17 строке мы инициализировали свойство DataAccessObject новым экземпляром класса StubFileDataObject. На 18 строке проверили правильность работы нашего метода FindLogFiles объекта FileManager. Объект StubFileDataObject мы уже рассматривали – он в теле метода GetFiles этого объекта создаёт коллекцию типа list с элементами типа string. Эта коллекция заполняется тремя элементами и ссылка на эту коллекцию возвращается из метода. При этом тест TestFindLogFiles у нас должен быть успешно пройден. Если мы захотим использовать не стаб, а реальный объект, чтобы получать доступ к файловой системе мы можем внедрять через объект не StubFileDataObject, а через FileDataObject, который действительно получает доступ к файловой системе. Тест у нас будет провален так как файла “file2.log” не существует в директории нашего проекта. Техника внедрения зависимости через свойство очень удобно пользоваться в тех случаях, когда для тестирования метода нам необходима только одна из зависимостей. И вот если зависимость имеет опциональный характер, то лучше использовать технику внедрения через свойство, чем через конструктор.

В этом примере мы попробуем внедрять зависимость через интерфейс. Особенность этого подхода заключается в том, что объект, который мы тестируем не будет хранить состояние внешней зависимости и внешняя зависимость будет внедрятся непосредственно перед вызовом тестируемого метода. В этом примере мы создали метода FindLogFile объекта FileManager. Ну теперь кроме имени файла, который мы хотим увидеть в нашей директории, мы также передаём и объект, который реализует интерфейс IDataAccessObject. Тут у нас как раз и внедряется зависимость. Логика метода FindLogFiles у нас отсталась такой, как и в предыдущих примерах, но также мы добавили проверку. Возможно пользователь захочет вызвать метод FindLogFile и передать туда в качестве второго параметра null. В таком случае логика метода FindlogFile не будет работать. Поэтому мы сначала проверим, если у нас dataAccessObject пустой, значит нужно сгенерировать исключительную ситуацию ArgumentNullException, если нет – выполнить те действия, которые находятся с 18 по 28 строки. Теперь давайте перейдём к тестовом методу и давайте посмотрим, как мы будем писать тест. Мы создали тестовый метод FindLogFileTest4 в теле тестового метода на 16 строке мы создали экземпляр класса FileManager, на 17 строке мы хотим проверить возвращаемое значение метода FindLogFile. Мы вызываем этот метод на объекте Manager и мы знаем, что у нас есть перегрузка, которая принимает два параметра: первый – это пусть файла, который мы хотим обнаружить, и второй – объект представляющий внешнюю зависимость. В данном случае мы будем заменять реальный объект, который работает с файловой системой на подставной объект StubFileDataObject. Данный тест будет успешно пройден, так как в данной коллекции, которую возвращает метод GetFiles объекта StubFileDataObject есть строка “file2.log”. Давайте запусти наш тест. TestFindLogFileTest4. Запускаем этот тест и он у нас успешно отрабатывает.

Этот пример нам продемонстрирует использование техники локального фабричного метода для внедрения зависимости в тестируемый класс. Локальный фабричный метод представляет собой локальный защищенный виртуальный метод внутри класса, который возвращает нужный экземпляр зависимости. В классе FileManger у нас уже есть базовая реализация локального фабричного метода, которая возвращает экземпляр класса FileDataObject, то есть того объекта, который работает непосредственно с файловой системой. Если мы захотим тестировать наш класс FileManager мы сможем создать новый класс который унаследуем от класса FileManager и самим переопределить локальный фабричный метод, для того, чтобы он возвращал экземпляр той зависимости, которую мы захотим. Обратите внимание, что внутри метода FindLogFile у нас создается экземпляр класса dataAccessObject: вот тут мы видим внедрение зависимости и создание зависимости у нас происходит с помощью локального фабричного метода, и дальше следует та логика, которую мы видели в предыдущих примерах. Теперь давайте посмотрим на класс наших тестов. В файле FileManagerTest мы создали класс FileManagerUnderTest, который унаследовали от класса FileManager. В новом классе-наследнике мы переопределили локальный фабричный метод таким образом, чтобы он возвращал экземпляр объекта StubFileDataObject. Теперь локальная фабрика будет возвращать не объект, который работает с файловой системой, а подставной объект, который внутри себя создает коллекцию с нужными нам объектами и возвращает их сюда. В теле тестового метода FindLogFileTest на 21 строке мы создали экземпляр класса FileManagerUnderTest, 23 строка: просто вызвали метод FindLogFile, получили результат, и на 25 строке проверили результат с помощью метода IsTrue объекта Assert.

В этом примере мы рассмотрим ещё одну технику внедрения зависимости, которая использует виртуальные члены и переопределения. Этот пример очень похож на предыдущий, за исключением того, что теперь для внедрения зависимости не потребуется извлечение интерфейса зависимости. Теперь логика работы с файловой системой полностью инкапсулирована в виртуальный метод FindLogFileExtension . Этот метод просто возвращает коллекцию типа list с элементами типа string. Базовый способ реализации этого метода использует объект FileDataObject, который обращается к файловой системе, но никто нам не запрещает при тестировании создать класс наследник от объекта FileManager и переопределить метод FindLogFileExctension таким образом, чтобы он возвращал коллекцию list с теми элементами , которые нам потребуются для тестирования нашего класса. При этом нам даже не понадобится создавать стаб объект, который будет реализовывать некий интерфейс зависимости. Давайте посмотрим на тестовый метод. В методе создается экземпляр касса FileManagerUnderTest – это класс, который мы унаследовали от базового класса FileManager. В этом классе-наследнике мы переопределяем метод FindLogFileExctension: тепер этот метод будет не использовать объект, который обращается к файловой системе, а будет просто создавать коллекцию типа list элементов типа string со следующими элементами: “file1.txt”, “file2.log”, “file3.exe”. В теле тестового метода после создание экземпляра класса мы вызываем метод FindLogFile, передаём требуемый файл, который мы хотим найти, и на 25 строке мы проверяем результат работы нашего метода. Давайте запустим этот тест – FindLogFileTest10. Тест успешно отрабатывает. Если мы захотим протестировать базовую реализацию метода FindLogFileExctension, то нам просто потребуется создать экземпляр класса FileManager.

В этом примере мы будем использовать фабричный класс для внедрения зависимости в FileManager. Теперь инициализаций поля DataAccessObject теперь будет заниматься статический фабричный метод CreateDataObject объекта FactoryClass. При вызове этого метода сперва будет проверки: если статическое поле DataAccessObject – не инициализировано, значит нужно использовать зависимость по умолчанию, то есть создать и вернуть FileDataObject. При тестировании можно будет вручную указать, экземпляр какого класса должен возвращать мтод CreateDataAccessObject класса FactoryClass. Для этого в теле фабричного класса есть метод SetDataAccessObject, который принимает значение типа IDataAccessObject тем самым инициализирует статическое поле объекта FactoryClass. При этом при последующем вызове метода CreateDataAccessObject уже будет возвращаться не новый экземпляр класса FileDataObject, который работает с файловой системой, а будет возвращаться элемент того класса, который мы передали качестве параметра метода SetDataAccessObject. Давайте перейдём к методу , который тестирует объект FileManager, и посмотрим как работать вот такой техникой. Сперва мы должны настроить фабрику таким образом, чтобы она возвращала экземпляр нужного нам объекта. Мы хотим для этого использовать подставной объект StubFileDataObject. Сперва на объекте FactoryClass мы вызвали метод SetDataAccessObject и передали туда новый экземпляр StubFileDataObject. Теперь файл менеджер будет пользоваться этим классом для получения списка файлов. Далее что нам потребуется сделать – это на 16 строке вызвать FindLogFile и передать туда имя файла, который мы хотим обнаружить. Потому как объект StubFileDataObject будет возвращать коллекцию, в которой есть “file1.txt”, то наш тест должен быть успешно пройден. Наш тест называется FindLogFileTest7. Давайте найдём этот тест, запустим его и убедимся в том, что он успешно отрабатывает.

В этом примере мы используем технику внедрения зависимости с помощью паттерна абстрактная фабрика. Этот способ будет напоминать метод внедрения зависимости через конструктор, как мы видели в первом примере внедрения зависимости. Если мы посмотрим на класс FileManager, то действительно, у объектов FileManager есть пользовательский конструктор, который принимает экземпляр IDataAccessObject, этим экземпляром инициализирует поле DataAccessObject класса FileManager. Дальше FindLogFile использует объект dataAccessObject, для того чтобы получить имена тех файлов, которые были обнаружены у нас в директории. То есть пока что это чистое внедрение через конструктор, но на самом деле разница заключается в том, что создавать и конфигурировать класс FileManager у нас будет абстрактная фабрика. В нашем проекте есть папка абстрактная фибрика, в который мы реализовали паттерн абстрактной фабрики. В классе IDataAccessObjectFactory мы реализовали интерфейс, которым должна обладать любая конкретная фабрика. Любая конкретная фабрика должна уметь создавать экземпляр класса, который реализует интерфейс IDataAccessObject. Далее мы реализовали две конкретные фабрики: ConcreteFactory и StubConcreteFactory. Конкретная фабрика ConcreteFactory у нас реализует интерфейс IDataAccessObjectFactory и поэтому у неё появляется метод CreateDataAccessObject. Фабрика ConcreteFactory будет возвращать будет возвращать новый экземпляр класса FileDataObject. Также у нас есть фабрика StubConcreteFactory, которая реализует тот же интерфейси будет возвращать новый экземпляр класса StubFileDataObject. Это подставной объект для тестирования. Также у нас есть класс Client, который нам позволяет создать экземпляр FileManager, используя для этого или объект FileDataObject, или объект StubFileDataObject. В клиенте у нас создаётся поле типа IDataAccessObjectFactory, то есть это поле, в котором будет храниться ссылка на конкретную фабрику. При создании экземпляра класса Client мы должны будем указать, какую фабрику использовать для создания экземпляра класса FileManager. При запуски метода Run мы будем создавать экземпляра класса FileManager и внедрять зависимость в этот класс с помощью метода CreateDataAccessObject нашей фабрики. Метод будет возвращать экземпляр FileManager приведённый к типу IFileManager. Теперь давайте посмотрим на тестовый метод, который будет тестировать метод FindLogFile объекта Manager. На 13 строке мы создаем экземпляр Client и указываем, что для создания объекта FileManager наш клиент будет использовать StubConcreteFactory. 15 строка: мы запускаем метод Run объекта Client, у нас создаётся экземпляр класса Manager и запускается метод CreateDataAccessObject фабрики, которую мы передали конструктору в качестве Client. Это фабрика StubConcreteFactory, которая на самом деле вернёт объект StubFileDataObject. Мы получим экземпляр класса FileManager, приведённого к типу IFileManager, и на 19 строке вызовем на нём метод FindLogFile и передадим туда имя файла, которое хотим обнаружить. Объект StubConcreteFactory будет возвращать коллекцию, в которой есть файл, который мы хотим обнаружить. Тест должен успешно выполниться. Запускаем тест и видим, что он успешно отрабатывает.

Как мы уже говорили, простейшим способом внедрения зависимости является внедрение через конструктор. У этого подхода есть проблема: состоит она в том, что у вашего класса может быть много зависимостей, и сигнатура конструктора станет слишком громоздкой, трудной для чтения. Другой способ – использования внедрения через свойство. Таким образом можно избавиться от длинного списка параметров конструктора. Однако, если у вас есть много зависимостей, количество кода очень быстро вырастет, в результате чего, если вы часто используете ваш класс, вам при каждом изменении класса придётся писать много кода для конфигурирования класса. Решение этой проблемы – использование контейнеров внедрения зависимостей, или же DI контейнеров. Контейнеры зависимости это компонент которые содержат определение зависимостей между объектами и при необходимости может создать их в соответствии с определениями. Существует множество различных DI контейнеров, тут продемонстрированы основные из них: StructureMap, Castle Windsor, Unity, Ninject. Мы с вами будем использовать Unity и Ninject в наших примерах. На этом слайде мы посмотрим как пользоваться DI контейнером с помощью Unity для создания класса FileManager, который мы с вами рассматривали на примерах. Сначала нам потребуется создать DI контейнер, что мы и сделали на первой строке нашего кода. Дальше потребуется зарегистрировать объекты и интерфейсы, которые это объекты реализуют для инстансирования зависимостей. После вызова метода RegisterType и регистрации объектов и интерфейсов нам можно создавать тот объект, в который мы хотим внедрять зависимость. Мы будет создавать объект FileManager с помощью метода Resolve нашего контейнера. При этом везде, где FileManager для своего создания будет требовать значение интерфейса типа IDataAccessObject будет инициализироваться новый объект типа StubFileDataObject и передаваться в качестве параметра конструктора. Тем самым мы избавимся от проблемы возрастания количества параметров конструктора. Инициализация объектов FileManager будет заниматься объект Container. Также мы с вами будем использовать DI контейнер Ninject для создания зависимостей объекта FileManager. Работа с Ninject осуществляется в два этапа: первый: привязка типов, с которыми требуется ассоциировать с созданными нами интерфейсами. В данном случае нам требуется указать Ninject, что при получении запроса на реализацию интерфейса IDataAccessObject нужно создать и возвратить экземпляр класса StubFileDataObject. Для этого мы применяем методы Bind, To, определённые в интерфейсе. Мы указываем интерфейс, который требуется зарегистрировать, используя его в качестве параметра обобщенного типа метода Bind, и передаём тип требуемой, конкретной реализации в качестве параметра обобщенного типа метода To. Второй этап – использование метода Get для создания объекта FileManager. Этот метод реализует нужные интерфейсы и передаёт их конструктору класса FileManager. Интерфейс, для которого требуется реализация мы указываем в качестве обобщенного параметра типа Get. Мы будем создавать экземпляр класса FileManager. Ninject просматривает определённые нами привязки, смотрит, что IDataAccessObject привязан к StubFileDataObject, и создаёт новый, уже сконфигурированный экземпляр класса FileManager.

В этом примере мы с вами попробуем создавать экземпляр FileManager, используя для этого контейнер Unity. Сперва, что нам нужно сделать, это определить, через что мы будем внедрять зависимость. Мы хотим внедрять зависимость через свойство. Для этого нам придётся свойство, через которое будет внедрена зависимость декорировать атрибутом Dependency, который определён в пространстве имён Microsoft.Practices.Unity, что мы и сделали на 17 строке нашего кода. Далее что нам потребуется сделать – создать контейнер экземпляр класса UnityContainer, на 21 строке мы регистрируем тип интерфейса и класса, который реализует этот интерфейс для инстансирования зависимости. Мы указываем, что при запросе объекта, который реализует интерфейс IDataAccessObject нужно подставлять новый экземпляр StubFileDataObject. На 26 строке мы будем создавать экземпляр класса FileManager не с помощью конструктора, а с помощью метода Resolve нашего контейнера, при этом Unity контейнер будет просматривать класс FileManager, он увидит, что есть свойство, которое ожидает внедрения зависимости и проверит, что на интерфейс IDataAccessObject у нас зарегистрирован StubFileDataObject. Тем самым мы создадим экземпляр класса FileMangaer с инициализированным свойством. 27 строка: мы запустим метод FindLogFile, передадим туда значение “file2.log” и наш тест должен успешно завершиться. Запускаем тест и видим, что тест у нас успешно отрабатывает и завершается. В следующем примере мы с вами посмотрим, как использовать Ninject DI контейнер. Давайте перейдём в класс FileManager.

В данном примере мы будем использовать метод внедрения зависимости через конструктор. В классе FileManager у нас есть пользовательский конструктор, который принимает значения типа IDataAccessObject. Далее нам нужно сконфигурировать Ninject модуль. Для этого мы создали класс ConfigFileObjectData, который унаследовали от класса NinjectModule. Мы переопределили метод Load объекта NinjectModule и указали, что при запросе интерфейса IDataAccessObject у нас должен создаваться новый экземпляр класса StubFileDataObject. Дальше мы перейдём в метод FindLogFileTest6 – это метод нашего теста, на 18 строке мы инициализируем Ninject. Для этого мы создадим экземпляр класса StandardKernel и передадим туда тот объект, который настраивает Ninject модуль для наших зависимостей. В итоге мы получим экземпляр класса StandardKernel который приведён к интерфейсу IKernel. 20 строка: для создания экземпляра класса FileManager нам потребуется вызвать метод Get на объекте ninjectKernel. Мы это сделали и тут же создаётся экземпляр класса FileManager, в качестве единственного параметра конструктора, которому будет передаваться экземпляр класса StubDataAccessObject. На 22 строке мы вызовем метод FindLogFile на объекте FileManager и проверим, действительно ли существует такой файл. Этот тест также должен успешно завершиться.

В этом и следующем примерах мы будем говорить оп проблемах инкапсуляции при создании дизайна, который проще тестировать. Как мы уже понимаем, набор тестов должен иметь доступ к тестируемому коду, то есть тестируемый код должен быть открытым. С другой стороны в ООП не должны нарушаться принципы инкапсуляции и закрытия данных. Если мы посмотрим на этот пример, то тут у нас создаётся класс FileManager с которым мы уже знакомы. При этом этот класс имеем два конструктора: первый конструктор у нас открытый и он инициализирует поле dataAccessObject новым экземпляром класса FileDataObject, который работает непосредственно с файловой системой. Специально для того чтобы было удобно тестировать наш класс FileManger и его метод FindLogFile мы создали пользовательский конструктор для этого класса, который принимает значение типа IDataAccessObject, то есть мы воспользовались паттерном внедрения зависимости через конструктор. И скорее всего мы не захотим, чтобы пользовать в коде смог пользоваться этим конструктором. Поэтому мы создали его internal. Но каким же образом тест сможет получить доступ к этому конструктору для создания экземпляра класса FileManager со стаб объектом? Для этого кончено же можно использовать рефлексию, но мы пойдём другим путём – мы будем использовать атрибут InternalVisibleTo, которым можно пометить сборку. При этом этот атрибут будет указывать, что все члены этой сборки, которые помечены модификатором internal, будут видны не только в этой сборке, но и в той сборке, имя которой мы указали в качестве позиционного параметра InternalVisibleTo. Если мы посмотрим на нашу папочку, то увидим, что у нас есть два проекта, которые относятся к этом примеру: это проект, который содержит непосредственно FileManager и объект FileDataObject и также у нас есть проект, в котором находится тест для сборки 011_Encapsulation. Этому проекту мы дали ссылку на проект 011_Encapsulation. При этом, если мы захотим пользоваться Internal членами сборки 011_Encapsulation, мы будем их видеть, потому как мы указали, что в сборке 011_Encapsulation FileManagerTest эти члены должны быть видны. В следующем примере мы будем использовать другой инструмент для решения этой проблемы.

У нас есть тот же класс FileManager, в котором есть конструктор по умолчанию, а также конструктор пользовательский, который нужен нам для удобного тестирования нашего кода. 22 строка: мы видим уже знакомый метод FindLogFile. В данном примере мы будем использовать директивы препроцессора if, else, endif для того, чтобы в отладочной версии нашего проета можно было использовать только конструктор, который принимает параметры типа IDataAccessObject. В релиз версии мы будем использовать конструктор, который не принимает параметров. В данный момент я использую дебаг версию данного приложения, поэтому мне доступен конструктор, который принимает один параметр, и я могу без проблем тестировать мой класс. При этом тело теста кончено я тоже буду разрешать использовать только в дебаг версии проекта, потому как если я выберу релиз версию проекта, то у меня не будет доступа к конструктору, который принимает параметр и просто напросто тест не будет компилироваться. Если я укажу релиз версию , то тело тесто у нас скрывается, а также в объекте FileManager скрывается конструктор пользовательский, и появляется конструктор по молчанию.

© 2017 ITVDN, все права защищены