Unit Testing в C# с использованием NUnit и NSubstitute. Часть 3

Unit Testing в C# с использованием NUnit и NSubstitute. Часть 1
Unit Testing в C# с использованием NUnit и NSubstitute. Часть 2

В третьей, заключительной части, посвященной unit test´ам применительно к C# рассмотрим практическую сторону интеграции и тестирования внешнего модуля для которого нет исходных кодов.
Использовать мы будем библиотеку Awesomium, которая является C# надстройкой над движком браузера chromium. Нужно спроектировать приложение которое позволяет программно открывать веб страницы во вкладках встроенного браузера. И позволяющее реагировать на действия пользователя (переходы по ссылкам, отправка форм). К примеру при нажатии на кнопку Поиск — сохранять все данные переданные по http (POST запрос) и на основании того какие данные были переданы — сохранять нужные параметры и отменять или нет запрос. При работе с веб страницами главная проблема — отработка скриптов и выполнение flash контента, эту проблему и позволит решить Awesomium, а дальше можно переходить на старые добрые HttpWebRequest/HttpWebResponse

Задача поставлена.
Перед тем как проектировать архитектуру я сделал небольшой прототип чтобы понять как работать с библиотекой Awesomium — выяснились довольно неприятные нюансы.
— хоть библиотека и позволяет запускать отдельные вкладки с раздельными кешами и хранилищами кукисов, но доступа к кукисам напрямую нет -только через js а это не дает получить к примеру httponly, поэтому если они нужны — прийдется забирать их напрямую из хранилища (представляет собой sqlite базу)
— так же минус в том что после создания сессии — ее параметры уже не поменять, только пересоздание с нуля
— для того чтобы перехватывать запросы — реализован интерфейс который необходимо проимплементировать и подставить в статическое свойство ядра AwesomiumWebCore. И в этом то и загвоздка — перехватчик один на всех, никак не привязать отдельный перехватчик на каждый WebBrowser, прийдется извращаться и строить обвязку

На основании этих данных я сделал предварительную прикидку архитектуры (заранее извиняюсь за ошибки в тексте, набирал на ipad, после экспорта было лениво править):
diagram

Начнем создание проекта, назовем его TabbedAwesomium, инструкция по созданию проекта вместе с проектом UnitTest´ов в первой статье, но создавать будем не консольное приложение а WinForm

Далее необходимо скачать и установить Awesomium (http://www.awesomium.com/download/), на момент написания статьи последняя версия была вроде 1.7.3
После установки — нужно добавить в Reference к проекту ссылки на Awesomium.Core и Awesomium.WinForm

Начнем с хвоста. Перехватом запросов должен заниматься файл унаследованный от интерфейса

Создаем MyInterceptor

После получения вызова OnRequest — его нужно как то обработать. Жестко прописывать такой код в класс MyInterceptor смысла не имеет, мы хотим гибкую систему. Хранить список делегатов внутри MyInterceptor тоже не очень хорошо пожалуй, так что будем посылать уведомление о том, что был вызов OnRequest классу, который в этом заинтересован.

Отдельно я выношу Id, т.к. при выполнении метода мы точно находимся в gui потоке и имеем доступ к свойствам ResourceRequest, так что лучше достать id, т.к. он нам будет нужен часто, чтобы не вызывать invoke в последствии для этого.

Протестировать мы можем по сути только то, что при вызове OnRequest мы генерируем событие RequestHandler. В коде я использовал DynamicEventArgs — моя обертка, позволяющая не создавать каждый раз класс унаследованный от EventArgs при добавлении нового EventHandler‘а

Да, могут появиться runtime ошибки если ошибешься с написание свойства, но боже как же муторно каждый раз создавать обертки.

Ладно, создаем в Unit Test проекте класс

И тут же налетаем на первый подводный камень, в метод OnRequest передается обьект класса ResourceRequest, у которого нет открытого конструктора. Более того все нужные нам свойства объявлены и реализованы прям в этом классе, а не имплементируют какой то интерфейс. Крайне неприятная ситуация. Прийдется создавать обертку вокруг ResourceRequest. Идея следующая: создаем интерфейс IResourceRequest и в нем определяем те методы и свойства которые мы планируем использовать. Создадим прокси класс ProxyResourceRequest, этот интерфейс реализующий и принимающий оригинальный ResourceRequest в качестве параметра.
На момент написания — мне понадобилось всего 1 свойство и один метод, что позволило не переопределять все полотно оригинального ResourceRequest.

Прокси класс вышел следующим:

Но у нас в OnRequest приходит оригинальный OnRequest, так что сделаем перегруженный метод который будет принимать уже наш интерфейс IResourceRequest, и вызывать его из старого метода OnRequest.
Код получается следующим:

Да, нам никак не протестировать оригинальный

, но т.к. мы по сути единственное, что делаем сейчас в этом методе — вызываем перегруженный, можно без опаски тестировать только его.

И мы получаем ошибки в строках

Спрашиваем совета у гугла и понимаем, что необходимо добавить reference на Microsoft.CSharp в проект с тестами. Добавляем, запускаем тест и ловим ошибку:
System.BadImageFormatException : Could not load file or assembly ‘Awesomium.Core, Version=1.7.3.0, Culture=neutral, PublicKeyToken=e1a0d7c8071a5214

Очередная сессия гугла и понимание, что надо выставить x86 для тестового проекта.

x86

Выставили, пытаемся запустить тест еще раз, снова ошибка.
Строка dynamic request = obj.ResourceRequest; вызывает ошибку
Microsoft.CSharp.RuntimeBinder.RuntimeBinderException : ‘object’ does not contain a definition for ‘ResourceRequest’

Ищем и понимаем, что все дело в том, что мы определяем динмические обьекты в одной сборке, а пытаемся использовать в другой. Т.к. у нас сборки не подписаны, для того, чтобы решить проблему- достаточно в проекте TabbedAwesomium в файле AssemblyInfo.cs добавить строку

Что позволяет сборке с юнит тестами видеть внутренние члены сборки с кодом.
Запускаем тест — он проходит.
Разберем, что же было в тесте.

Для начала мы создали фейк для интерфейса IResourceRequest

и указали, что при вызове у него свойства ViewId — вернется указанное нами значение

Затем создали MyInterceptor и подписались на его событие RequestHandler

В делегате его обрабатывающем мы просто присваиваем viewId значение из переданного ResourceRequest, чтобы удостовериться, что все работает.
Дальше мы делаем вызов OnRequest и проверяем что мы получили то, что ожидали

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

Создадим еще один класс BrowserIdToDelegateBridge, при именовании класса муза покинула меня, зато явно понятно, что делает этот класс — связывает конкретный браузер с конкретным делегатом (в котором и определяется реакция на ResourceRequest)

Этот класс должен позволять добавлять связь браузера и делегата, удалять, и собственно по id браузера получать нужный делегат.

Напишем первый тест, который покажет, что после того как мы сделали привязку делегата к id браузера — данный делегат будет вызван. Но при обдумывании теста сталкиваемся снова с проблемами. Обработчик вызовов MyInterceptor необходимо присвоить свойству ResourceInterceptor ключевому обьекту библиотеки Awesomium — статическому классу WebCore. Да, опять прийдется писать обертку, мы же не хотим в самом деле в тестах загружать тяжеловесную библиотеку для каждого теста.

Если сравнивать с ProxyResourceRequest, то используется немного другая техника, т.к. WebCore — статический класс. При использовании в реальной программе при обращении к методу Instance(), если instance не был проставлен через свойство Shared — создается новый обьект WebCoreProxy, которые проксирует обращения к реальному статическому классу WebCore. Если же мы присвоим Shared наш фейк, — вызовы будут проксироваться через него, что позволит тестировать.

Запускаем тест и конечно получаем ошибку, реализации класса BrowserIdToDelegateBridge пока нет.

Храниться привязки будут в словаре

Мы будем делать потокобезопасный класс, потому нужен обьект по которому будем лочить

Cодержимое InitInterceptor будет следующим

Обьясню почему мы проводим инициализацию именно здесь. С одной стороны — инициализация ResourceInterceptor должна производиться как можно раньше. При создании какого нибудь высокоуровневого контроллера. С другой стороны MyInterceptor используется только этим классом, более того, делегат обрабатывающий RequestHandler — вызывает методы этого класса, таким образом класс BrowserIdToDelegateBridge получается монолитным решением для работы с ResourceInterceptor.
К сожалению не получится вызвать InitInterceptor из конструктора, как казалось бы и стоит сделать, т.к. WebCore на этот момент еще не инициализирован, и произойдет это только после создания первой view. Вызов метода BindIdToDelegate гарантирует, что т.к. есть id какой то view, то WebCore был уже инициализирован и можно ему проставить Interceptor

Запускаем тест, все работает без сбоев.

Думаю стоит создать еще один тест, который тестирует, что BrowserIdToDelegateBridge корректно вызывает нужный delegate по id.

Мы сделали 2 привязки к разным id — разные делегаты и проверили что вызывая OnRequest с определенным ResourceRequest‘ом, — вызываются корректные делегаты. Обращаю ваше внимание на то, что в одном тесте я сделал 2 проверки Assert‘ом. В принципе это считается дурным тоном, делать более одной проверки в одном тесте. Но на мой взгляд, в данном случае это оправдано, какой бы из Assert‘ов не обвалился — это будет показывать о том, что сломался один и тот же механизм.
Если тестировать к примеру вызов только с одним из ResourceRequest, может получиться что тест не покажет ошибки хоть она и есть. Представим что кто то впоследствии изменит реализацию класса BrowserIdToDelegateBridge и вместо словаря сделает простое присвоение с перезаписью. Если бы мы в тесте проверяли вызов OnRequest только с secondResourceRequest — ошибки бы не возникло, но на самом деле она бы присутствовала.
Ну а дублировать тесты, чтобы в одном проверить вызов для firstResourceRequest, в другом для secondResourceRequest — считаю излишеством. По моему главная проблема любой техники — люди пытаются довести ее применение до 100%. Но, к счастью, мы занимаемся творческой работой, программируем, а не заучиваем псалмы. Нужно пытаться удерживать баланс между строгостью и гибкостью. Это не просто, более того, идеального решения не существует. Кто то решит, что вызов всего одного Assert‘а — священен, и чтобы не дублировать код — вынесет инициализацию в отдельный метод, и сделает два теста вызывающих этот метод, в одном проверит вызов с firstResourceRequest а в другом с secondResourceRequest. И тоже будет по своему прав. Но по моему мнению, в данном случае — это уже перебор.

Стоит написать следующие тесты:
— удаление привязки с отсутствующим id — не вызовет ошибок и ничего не удалит
— добавление привязки с тем де id не вызовет ошибки и перезапишет старое значение
и т.д.
И хоть на первый взгляд эти тесты не столь важны, т.к. мы по сути этим тестируем поведение Dictionary, а не BrowserIdToDelegateBridge. Но ведь если кто то поменяет реализацию BrowserIdToDelegateBridge мы можем неслабо отгрести. Не стоит забывать, что unit test‘ы нужны не столь для того, чтобы помогать нам отлавливать баги на момент написания, — сколько помогают их мгновенно отлавливать в дальнейшем.
Я писать эти тесты сейчас не стану, но заметку на потом сделаю.

Создадим новый UserControl и назовем его BrowserControl. Перетаскиваем на него WebControl, AddressBox и кнопку.
WebControl

Указываем для AddressBox наш WebControl Должно получиться что то вроде этого:

connectAddressBar

И добавляем в него методы необходимые для работы.

Т.к. WebControl имеет ленивую инициализацию, мы можем получить его Identifier наверняка только после того как наш UserControl отобразиться, поэтому нельзя прям из конструктора вызвать BindIdToDelegate.

Напомню — в схеме что я прикладывал, предполагалось, что реальный WebControl будет создаваться внутри класса BrowserControl, являющегося производным от UserControl. который также будет лишь вкладкой в BrowserForm. Так вот, я вспомнил, что вкладка с веб браузером должна закрываться автоматически, в случае если в переданном делегате прошли тестовые условия. А у меня это к сожалению не предусмотрено. Делегат который передается извне имеет сигнатуру Action<IResourceRequest> checkRequest;
В него не передается userControl, чтобы можно было вызвать Close, BrowserIdToDelegateBridge тоже ничего не знает какому UserControl‘у принадлежит тот или иной viewId. Что ж, прийдется менять архитектуру.

Варианты вижу следующие
— при вызове BindIdToDelegate из класса BrowserControl — передавать так же ссылку на сам BrowserControl (вместо делегата), чтобы прикрепить его к словарю. И когда срабатывает resourceInterceptor.RequestHandler — вызывать метод у контроллера полученного по id. Но в таком случае BrowserIdToDelegateBridge будет знать внутреннее устройство BrowserControl, что на мой взгляд не очень хорошо.
— второй вариант — завести событие, внутри BrowserIdToDelegateBridge — к примеру — NeedClose. Но не стоит забывать, что на одно и то же событие получается будут подписаны множество BrowserControl. Поэтому прийдется передавать в качестве аргумента — контроллер а в BrowserControl при подписывании проверять на то, что событие предназначалось нам.

Я предпочту второй вариант. Так что видимо прийдется BrowserIdToDelegateBridge переименовывать в BrowserIdToControlBridge, и хранить не сам делегат а BrowserControl его содержащий

Получится что то вроде этого

Совсем плохая новость — наши тесты BrowserIdToDelegateBridgeTests не то что падают — не компилируются, т.к. вместо Action‘а мы передаем Control в реальном коде. Переписывание тестов заняло минут 10

Что поменялось — если раньше мы делали фейковые action‘ы и биндили их к viewId, и внутри этих action прописывали нужные флаги, то теперь мы подписываемся на Event Intercept и там проверяем с каким control’ом он был вызван, и в зависимости от этого выставляем флаг. В какой то степени изменение пошло на пользу классу BrowserIdToDelegateBridge, он теперь ничего не знает про action’ы, он теперь действительно просто мост, и когда срабатывает RequestHandler — вызывает EventHander с нужным контролом.
Изначально я проектировал биндить в классе BrowserIdToControlBridge не object к id, а UserControl, но потом подумал что это излишество, в тестах не пришлось создавать UserControl, хотя неприятно что иногда приходится делать cast

Обнаруженная ошибка проектирования заставила меня еще раз пересмотреть архитектуру и всплыли новые проблемы. Мы имплементируем снизу вверх, и это чревато тем, что при плохо продуманной архитектуре — ее приходится сильно менять.
Вот к примеру, — основная цель этой программы какая? Открывать во вкладках нужные веб страницы, вручную производить какие то действия и если результат устраивает — передавать наверх все нужные данные для дальнейшей обработки. А у нас передается в контроллер Action, который по сути ничего не возвращает, мы выполняя его никак не можем узнать, подходящие ли условия или нет для прерывания запроса.
Вывод — надо менять Action<IResourceRequest> на Function<IResourceRequest, bool>
Идем дальше, в каком бы классе не срабатывал делегат подписанный на bridge.Intercept — нам в итоге надо будет передать данные выше, иначе какой смысл все было затевать. В этот раз я завис на пару часов перебирая варианты.
У нас по сути получается асинхронный код. Откуда то извне мы вызываем общий conroller и просим его создать вкладку с определенной url. Если не предпринимать никаких действий — код вернет управление после создания формы с браузером. Мне же в моем случае асинхронность только во вред, т.к. код должен продолжаться с учетом того успешно или нет отработал браузер, и с учетом данных которые получились.
Т.е сейчас код выглядит так

….
1) старый код
2) вызов controller.AddTab(url, …)
3) продолжение старого кода который будет работать с ResourceRequest
….

Если же нигде не ожидать окончания работы в controller, в момент выполнения 3го шага — у нас еще нет никаких данных, браузер то только открылся и никакой interceptor не отработал.

Первый вариант — ловить SuccessHandler и в нем продолжать выполнение. Но в конкретной ситуации вызов controller.AddTab не является обязательным, т.е. код по хорошему быть таким

Что порождает дублирование, выносить «продолжение старого кода» в отдельный метод — вариант, но это потребует лишних телодвижений. Хотелось бы все таки работать в старом добром стиле: вызвал метод, получил результат, продолжил выполнение кода.

Можно после вызова Add останавливать выполнение кода, до тех пор пока controller не выкинет SuccessHandler. Останавливать можно как руками, так и к примеру AutoResetEvent

Так намного лучше, но копнув глубже обнаруживаем проблему. Мы подписываемся на SuccessHandler из разных потоков, но как определить какой именно поток должен обрабатывать конкретный вызов SuccessHandler? Привязываться к обьекту который вызвал Add? Можно но только в том случае если всегда вызывающий код будет останавливать свое выполнение в ожидании завершения. А если нет? Тогда один вызов SuccessHandler с обьектом переданным для проверки — продолжит выполнение сразу нескольких потоков.
Привязываться к threadId? Та же проблема.
В итоге я решил при каждом вызове генерировать уникальный id, и возвращать его, параллельно занося его в наш bridge. Ну а код будет что то типа такого:

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

Что вырисовывается
BrowsersContainerController — основной контроллер. При создании — подписывается на событие Intercept нашего моста. При создании вкладки генерирует uniqueId и отправляет его с переданными данными в BrowsersContainer.
BrowsersContainer — наша форма содержащая вкладки с браузерами. При создании вкладки — создает BrowserControl и добавляет его на форму
BrowserControlUserControl который создает браузер и после создания — биндит все данные к browser.ViewId (BrowserControl, Func<>, uniqueId)

Наш bridge теперь опять содержит Func<>, еще и uniqueId впридачу. Мне это не очень нравится, с другой стороны ничего страшного в этом нет, плодить несколько мостов по моему точно смысла нет.
Хотя зря получается переписывали тесты, т.к. мост опять содержит делегат.
Мост сейчас выглядит следующим образом

И тесты в классе BrowserIdToDelegateBridgeTests опять надо менять ) Не вижу смысла приводить полный текст, по сути надо добавить в вызовы bridge.BindIdToObjects(secondViewId, secondFakeControl) заглушки вместо 3го и 4го параметров, т.к. мы их не тестируем.

Код BrowserControl теперь следующий

И хотя тестировать тут можно сказать нечего в данный момент, мне не очень нравится, что гуишный класс в курсе о мосте, уникальных id и прочем. Уже на момент проектирования мне это не нравилось, но альтернативой я видел только формирование еще одного словаря внутри основного контроллера, чтобы сопоставлять для какого конкретно BrowserControl был перехвачен EventHandler. У меня совсем вылетело из головы замечательное свойство у анонимных делегатов — захватывать переменные «в плен».
Типа такого:

BrowserControl упрощается максимально

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

Т.к. события BrowserOnLoadingFrameComplete и BrowserOnLoadingFrameFailed срабатывают многократно (при загрузку каждого файлика), я делаю проверку на IsMainFrame, чтобы реагировать только на результат загрузки основного фрейма. Ну и в BrowserOnLoadingFrameComplete не реагирую на загрузку страниц с адресами «about:blank» и «chrome://chromewebdata/», т.к. при появлении окна сразу же получаем сообщение, что окно с адресом «about:blank» было загружено, что нам в принципе не интересно.

Если касаться тестирования, то тестировать FireEvent смысла особого не вижу код максимально простой и менять его смысла особого не должно быть в будущем. BrowserOnLoadingFrameComplete и BrowserOnLoadingFrameFailed протестировать все таки можно. В этих методах хоть какая то логика имеется. сначала я думал сделать их public, но прокрутив в голове дальнейшие шаги и вспомнив проблемы возникшие с ResourceRequest, посмотрел можно ли создать экземпляры LoadingFrameFailedEventArgs и FrameEventArgs, как я и боялся — нельзя, так что пойдем проторенным путем с перегрузкой метода и созданием прокси классов.

ProxyLoadingFrameFailedEventArgs на данный момент избыточен, но если нам понадобятся в итоге свойства из оригинального LoadingFrameFailedEventArgs мы всегда сможем их добавить.

Код внутри BrowserControl меняется не сильно

Напишем тесты сначала для BrowserOnLoadingFrameFailed, проверим, что событие генерируется или нет в зависимости от того, что содержится в IsMainFrame
При попытке создании в тесте BrowserControl сразу же пошло предупреждение что нужно добавить в Reference ссылку на System.Windows.Forms, не хотелось конечно но что поделать.

Для BrowserOnLoadingFrameComplete мы напишем те же тесты, но плюс к ним — тест который проверяет что событие Complete не генерируется если url из тех, которые мы фильтруем. Надо написать минимум 5 тестов, чтобы все это проверить. И здесь стоит применить технику из 2й статьи — параметризированные тесты.

Ну а с адресом все просто.

Дальше пришлось все таки отойти от канонов TDD, и чтобы по 100 раз не переписывать тесты — набросать прототип поверх уже всего написанного, споткнуться о то, что все что связано с Awesomium нужно выполнять в GUI потоках, исправить это. В общем класс TabsForm был написан раньше тестов.

Суть этой формы довольно проста — она содержит 3 вкладки Loading, Complete, Failed, внутри каждой — вкладки с браузерами. При добавлении новой вкладки — она попадает в состояние Loading, в зависимости от того успешно или нет она загрузилась, или может адрес сменился в результате перехода — мы перемещаем в соответствующие вкладки, Complete, Failed или обратно в Loading.
Последние 3 метода: TabLoadingTitle, TabFailedTitle, TabCompleteTitle были написаны специально для тестирования. Я посчитал выставлять наружу сами контроллеры вкладок слишком сильно нарушит инкапсуляцию, а заголовки являются отражением внутреннего состояния соответствующих вкладок.
Если по коду — присутствуют грабли

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

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

Что нам осталось? Да по сути два класса, основной контроллер BrowsersController и вспомогательный класс SessionsController, помогающий ограничивать число одновременно открытых вкладок. Он просто возвращает свободный слот (число в виде строки), на основании которого строится путь к кешу для данного браузера. Я не стал реализовывать возможность динамически менять число сессий, в принципе ничего особо сложного в этом нет, просто мне без надобности.

Интерфейс простейший. Что можно протестировать
— что слотов отдается не больше чем указано в maxSession
— что после возвращения слота — он вновь доступен

Что ж, осталось последнее, — класс BrowsersController. Он должен быть в единственном экземпляре, так что будет синглетоном.
Какие за ним обязанности? Именно он обрабатываем Intercept событие от BrowserIdToObjectBridge, в котором если все успешно — удаляет вкладку с браузером и отправляет данные вызвавшему обьекту. Так же он позволяет добавить вкладку с браузером. Ну и он же должен биндить в нашем мосте все данные.
Основной метод будет с такой сигнатурой

Что должно быть внутри метода? Создание BrowserControl‘а — и добавление его в форму, так же здесь же мы должны забиндить данные в наш BrowserIdToObjectBridge.
Как проверить корректность? Пожалуй что проверив, что в форму добавляется BrowserControl с тем же url. Чтобы иметь возможность это сделать нужно извлечь интерфейс из TabsForm и добавить сеттер с TabsForm в BrowsersController.

В будущем сделать фейк для формы будет уже проще, надо будет интерфейс наследовать от IForm и добавлять только новые методы.
Пишем тест

Пишем код класса BrowsersController

Проверяем — тест проходит. Но есть один нюанс. В коде мы создаем сессию с помощью вызова метода
CreateWebSession. Т.к. мы вызывает этот метод с путем, на самом деле создается папка «cache/0», что мягко говоря огорчает. Так что будем в тесте подставлять фейк.

А вот теперь тест проходит и веб сессия создается в памяти, папки с кэшем не создается.
В конструкторе BrowsersController мы также должны подписаться на событие Intercept у моста. Именно при обработке этого события мы должны завершать работу конкретного браузера по необходимости.
Так же надо возвращать интересующие нас данные.
На момент срабатывания события Intercept мы знаем о BrowserControl, но ничего не знаем об обьекте который им владеет, в нашем случае об TabPage. Но BrowserControl — это наследник от UserControl, соответственно нам доступно свойство Parent. Это позволяет нам написать такой код в TabsForm:

И на основании этого метода написать обработчик Intercept

Но чтобы протестировать этот код, нам надо, что бы в мост были добавлены данные, пришлось немного отрефакторить AddTab

Забавно, что при написании теста для проекта TabbedAwesome.UnitTests пришлось в доверенные добавлять TabbedAwesome, т.к. это первый тест где мы начали пользоваться DynamicEventArgs.

Пожалуй это самый сложный тест из написанных и он потребовал некоторых изменений в классе BrowserIdToObjectBridge.
MyInterceptor теперь создается во время создания BrowserIdToObjectBridge, но теперь его можно заменить

Таким образом можно подставить фейк, и уже на фейке реальный код в методе InitInterceptor устроит подписку на RequestHandler

Вызов browserControl.BrowserIdReady.Invoke в тесте позволяет отработать биндингу в мост.
Ну а вызов browsersController.Bridge.ResourceInterceptor.RequestHandler.Invoke — эмулирует, что пришел сетевой запрос который надо эмулировать, и т.к. в качестве func мы передали делегат который всегда отдается true, — тут же сработал код завершающий работу данного браузера.

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

Для проверки нашего кода я создал форму с кнопкой, при нажатии открываются 10 вкладок с mail.ru, можно пронаблюдать как они сначала перемещаются во вкладку загружаемых, затем по мере загрузки переходят в загруженные. А если заполнить форму логина и нажать кнопку отправки — вкладка с браузером закроется и в консоли выведется POST строка запроса.

Чтобы получить доступ к UploadElement у IResourceRequest — добавил в интерфейс и реализацию код:

Код проекта можно скачать здесь

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

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