Создание приложения на WPF с использованием принципов TDD. Часть 1Источник: codingclub
В этой части мы создадим скелет нашего приложения. И первый вопрос, который необходимо решить, это определиться с названием приложения. Есть open-source проект WixEdit, схожий по назначению. Но мы на создание и редактирование проекта не замахиваемся, мы будем только отображать существующий проект, и основная функция будет сборка дистрибутива. После недолгих раздумий принимаем решение назвать проект WixMaker. Создаем в Visual Studio новый проект WixMaker из шаблона WPF Application и добавляем в него ссылки на сборки Composite Application Library. По шагам процесс создания проекта с использованием CAL описан в How to: Create a Solution Using the Composite Application Library. Также подсоединяем проект к системе контроля версий, например SVN Это будет главный подпроект, который будет заведовать только сборкой модулей и запуском основного окна приложения. Все эти действия будет выполнять класс Bootstrapper. Основная функциональность будет разбита на слабосвязанные модули. Создадим макет внешнего вида приложения, а точнее разобьем содержимое главного окна на регионы. Минимальный набор это меню и основная часть, где выводится содержимое проекта. В первом приближении компоновка окна следующая: <Window x:Class="WixMaker.Shell" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:cal="clr-namespace:Microsoft.Practices.Composite.Wpf.Regions;assembly=Microsoft.Practices.Composite.Wpf" Title="WixMaker" Height="400" Width="600"> <Grid> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <ItemsControl Name="MenuRegion" cal:RegionManager.RegionName="MenuRegion" /> <ContentControl Name="MainRegion" cal:RegionManager.RegionName="MainRegion" Grid.Row="1" /> </Grid> </Grid> </Window> Пока на этом остановимся и займемся модулем. Создадим модуль, который будет заведовать показом содержимого WiX проекта и обработкой команд из меню. Делаем новый подпроект WixProject и в нем класс WixProjectModule. Он будет служить основным связующим звеном для связи содержимого модуля с внешним миром. Теперь мы наследуем класс WixProjectModule от интерфейса IModule, и с помощью IntelliSense создаем реализацию единственного метода этого интерфейса Initialize. #region IModule Members public void Initialize() { throw new NotImplementedException(); } #endregion Также пропишем наш модуль в загрузчике. public class Bootstrapper : UnityBootstrapper { protected override DependencyObject CreateShell() { Shell shell = Container.Resolve<Shell>(); shell.Show(); return shell; } protected override IModuleEnumerator GetModuleEnumerator() { return new StaticModuleEnumerator(). AddModule(typeof(WixProjectModule)); } } Это последний шаг, который мы может сделать без тестов. Далее создаем тест для WixProjectModule для метода Initialize, воспользовавшись поддержкой Visual Studio Team System. Щелкнем на названии функции Initialize() и из контекстного меню выберем пункт "Create Unit Tests...". Согласимся с подтверждающим диалогом, а в диалоге создания нового проекта изменим название проекта на WixProject.Test. Здесь мы следуем методике Microsoft, и называем подпроект с тестами по имени подпроекта, который он тестирует, с добавлением ".Test" в название. Далее студия создаст проект, класс теста для WixProjectModule и тестовый метод InitializeTest() и начальным содержимым. /// <summary> ///A test for Initialize ///</summary> [TestMethod()] public void InitializeTest() { WixProjectModule target = new WixProjectModule(); // TODO: Initialize to an appropriate value target.Initialize(); Assert.Inconclusive("A method that does not return a value cannot be verified."); } Мне очень нравится поддержка со стороны Visual Studio в данном случае. Во-первых, проделана большая рутинная работа, а во-вторых, в сгенеренном коде нет ни одной лишней строчки. Теперь надо решить, что мы будем реализовывать и соответственно проверять. Пойдем по пути наименьших усилий и добавим меню. Так как после добавления меню должно появиться в регионе MenuRegion, то давайте будем проверять, что там что-то появилось. Но для этого надо сделать несколько подготовительных операций. Для начала надо откуда-то получить ссылку на RegionManager, куда мы будем добавлять меню. Это делается просто: private readonly IRegionManager _regionManager; public WixProjectModule(IRegionManager regionManager) { _regionManager = regionManager; } Не удивляйтесь, но этого будет достаточно. Если вы поставите точку останова в конструкторе и запустите проект, то увидите, что regionManager содержит реальный объект. Первый раз, увидев такой код в выдернутом из недр интернета примере, я ничего не понял. Откуда возьмется реализация IRegionManager? Собственно это и явилось побудительной причиной познакомиться с Composite Application Library. Здесь мы в первый раз в нашем проекте сталкиваемся с паттерном Inversion of Control (IoC) который в данном случае реализован через другой, более специализированный паттерн Dependency Injection (DI). А это значит, что всю необходимую работу для нас делает Unit Container. Помимо того что мы избежали внесения сильной связи в наш модуль, а то и еще хуже Singleton, мы получили отличную возможность для модульного тестирования, чем мы сейчас и воспользуемся. Для этого в нашем тесте вместо реального RegionManager мы передадим поддельный объект (Mock object) с необходимой нам для теста функциональностью. public class MockRegionManager : IRegionManager { private Dictionary<string, IRegion> _regions = new Dictionary<string, IRegion>(); public IDictionary<string, IRegion> Regions { get { return _regions; } } public void AttachNewRegion(object regionTarget, string regionName) { throw new NotImplementedException(); } public IRegionManager CreateRegionManager() { throw new NotImplementedException(); } } Как видите, код подделки прост. И он и не должен быть сложным. Мы не пытаемся полностью сымитировать поведение объекта, а только ту минимальную часть, которая нужна нам для конкретного теста. Если для другого теста нам понадобится дополнительная функциональность, надо будет оценить, что лучше: дописать новую функциональность в уже существующий поддельный объект, с вероятностью, что это затронет уже отлаженные тесты. Или написать новую реализацию поддельного объекта, только с той функциональностью, которая нужна для нового теста. Подобным образом реализуем поддельный объект для IRegion. Теперь у нас есть все для первого теста. Возвращаемся к нашему тесту, а конкретно к строчке с TODO, который предлагает нам дописать код инициализации. Как видим, комментарий был поставлен не зря. Заменяем эту строчку на var regionManager = new MockRegionManager(); WixProjectModule target = new WixProjectModule(regionManager); Также добавляем поддельный регион для меню var menuRegion = new MockRegion(); regionManager.Regions.Add("MenuRegion", menuRegion); С инициализирующей частью теста мы закончили, займемся проверкой результата. Модуль должен добавить объект меню в регион "MenuRegion". В детализацию о типе объекта я не хочу вдаваться, поэтому будет достаточно проверки, что какой-то объект добавлен в регион. Assert.AreEqual(1, menuRegion.AddedViews.Count); Полностью тест выглядет так [TestMethod] public void InitializeTest() { var regionManager = new MockRegionManager(); WixProjectModule target = new WixProjectModule(regionManager); var menuRegion = new MockRegion(); regionManager.Regions.Add("MenuRegion", menuRegion); target.Initialize(); Assert.AreEqual(1, menuRegion.AddedViews.Count); } Тест готов. Нам надо его запустить, чтобы получить первоначальный негативный результат. Это важный момент. Если вы сначала напишите функциональность, а потом тест, и он сразу пройдет, то это не значит, что новый кусок кода работает правильно. Может быть, тест проходит и без него. Запустить тест можно или из контекстного меню, встав внутрь тестового метода и выбрав "Run Tests" или через меню Test->Run->Test In Current Context.Получаем первый результат Полностью Error Message: Test method WixProject.Test.WixProjectModuleTest.InitializeTest threw exception: System.NotImplementedException: The method or operation is not implemented.. Ну что же, мы получили красный цвет и конкретное сообщение, которое указывает что делать дальше. Создаем MainMenu класс, отнаследованный от UserControl. <UserControl x:Class="WixProject.MainMenu" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Grid> <Menu Grid.Row="0" IsMainMenu="True"> <MenuItem Header="File"> <MenuItem Header="Open" /> </MenuItem> </Menu> </Grid> </UserControl> Наконец добрались до сути и добавляем наше меню в регион public void Initialize() { IRegion menuRegion = _regionManager.Regions["MainMenu"]; menuRegion.Add(new MainMenu()); } Здесь мы использовали еще одну специализированную реализацию шаблона IoC, а именно шаблон Service Locator. Запускаем заново тест, и получаем опять красный цвет и сообщение: Test method WixProject.Test.WixProjectModuleTest.InitializeTest threw exception: System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.. Так, ситуация проясняется. Мы ошиблись в названии региона. Если бы не тест, мы при запуске приложения просто бы увидел пустое окно, и нам бы просто оставалось гадать, в чем дело. Исправляем название региона на MenuRegion, запускаем тест снова и наконец видим зеленый цвет. Запускаем наше приложение, видим окно приложения с меню. Да, кажется, по функциональности мы не сильно продвинулись. На самом деле мы создали вполне работоспособный и главное легко расширяемый макет приложения, который мы и будем наполнять. |