В статье описаны общие принципы построения приложений, с использованием подхода MVC, на примере внедрения в приложение, использующее много лет Code Behind подход.
Не будет:
• Классического Unit Test;
• Принижения влияния Code Behind;
• Неожиданных открытий в MVC.
Будет:
• Unit Test и Stub;
• MVC;
• Внедрение подхода на базе существующего продукта.
Перед тем, как начать использовать данных подход, я прошел большой путь обычного разработчика корпоративных систем. По пути мне встречались абсолютно шаблонные события и вещи, как простые, так и более сложные. Например, разработка закончена, функциональность отправлена на тестирование, кликается, далее валится масса ошибок, от простых недочетов, до грубых нарушений. Более сложный случай: функциональность принята, пользователи активно участвуют в обратной связи и приводят к необходимости менять функционал. Функционал поменяли, пошли по тому же пути и перестал работать старый функционал, да и новый оставляет желать лучшего. Кое как прищёлкивая быстрорастущую функциональность начинается постоянная правка багов. Сначала это были часы, потом недели, сейчас я знаю, что и месяцы не предел. Когда это занимало часы или дни, ошибки закрывались, все более – менее текло. Но стоило начать переоткрывать ошибки – начинался ад. Отдел тестирования постоянно ругается, разработчики постоянно правят ошибки и делают новые в разрабатываемом функционале. Все чаще приходит обратная связь, от реальных пользователей, полная негатива. А функционал рос и рос, я искал методы и подходы, как можно себе упростить жизнь. При этом меня давно посещала идея, задействовать при разработке пользовательского интерфейса тестирование на уровне разработчика. Желание заложить пользовательские сценарии привело меня к осознанию факта наличия проблемы: отсутствия хоть сколько-то объективных критериев готовности продукта от разработчика. И я начал мыслить несколько шире, а что, если постараться заложить все положительные сценарии использования в некоторые правила, постоянно проверять наборы правил, ведь это хоть какая-то гарантия того, что пользователь на следующем выпуске не будет ошарашен отвалившимся функционалом, а тестировщики будут задействованы по назначению. Именно этим правилам и предрекалось стать критерием окончания работы над функционалом для себя, для разработчика.
Начались поиски инструментов, для формализации правил. Весь код логики взаимодействия с пользователем и данными находился в формах. Конечно же, были отделены слои работы с базой данных, сервисы, ремоутинги и многое, многое другое. Но пользовательский интерфейс оставался перенасыщен логикой. Логикой, никак не связанной с отображением. Первым, и вполне логичным шагом, мне показалось использовать средства для визуального тестирования форм. Набираем правило их кликов – вводов и преобразуем это в тест. Для примера это прекрасно умеет делать Visual Studio 2013 Ultimate, вернее один из её шаблонов. Я бодро создал несколько тестов и понял, что создавать таким образом тесты, а потом запускать, огромная беда. Требовалось кучу времени чтобы полностью загрузить всю систему, дойти до нужных диалогов и вызвать соответствующие события. Это требовало работу всех звеньев цепи, что местами было невозможно. Все действия я предпринимал абсолютно отделив для себя Unit Testing, при этом его постоянно использовал для сервисов и логики, которая вращалась на сервере, ведь у неё не было форм, соответственно Code Behind. Параллельно работая с ASP.NET MVC мне все больше нравился подход с отделением логики от представлений. Упрощенно, вызывая контроллер ASP.NET MVC мы просто дергаем его публичный метод, который возвращает представление с некоторыми изменениями. Я начал думать, как эту модель переложить на WinForms и пришел к выводу, что используя события на представлениях я буду вызывать методы контроллеров. Я принял правило, что у меня всегда есть класс-контроллер, который привязывается к событиям класса, реализующего интерфейс представления. Тем самым я убрал весь код логики из Code Behind в код контроллера, а контроллер подлежал тестированию с помощью обычных Unit Tests, реализуя Stubs для классов менеджеров данных и самих представлений. Позже я научился тестировать и сами представления, исключительно с помощью Unit Tests. Далее я хочу показать пример того, как я пришел к этому.
Все описанное выше я рассмотрю на примере реализации формы изменения исключений при импорте данных. Исключения это просто сущность с тремя полями, необходимая для чтения текстовых файлов в системе. С точки зрения пользователя данная форма должна позволять добавлять, изменять, удалять исключения. После добавления нового исключения оно должно добавляться в конец списка. При удалении исключения фокус должен переходить на первый элемент списка. При этом форма вызывается из контекста никак не реализующего MVC.
Воспитанный на огромном количестве форм, в корпоративных приложениях, я пришел к выводу, что тоже буду использовать стандартный подход к интерфейсу: панель управления с кнопками — действиями над исключениями, список исключений, в виде таблицы, и базовой кнопкой закрыть. Соответственно для отображения в интерфейсе мне понадобится:
• Весь список исключений, для построения таблицы;
• Исключение в фокусе;
• Событие смены исключения в фокусе;
• Событие добавления;
• Событие удаления;
• Событие нажатия кнопки закрыть;
• Метод открытия;
• Метод закрытия;
• Метод обновления представления.
В дальнейшем я понял, что для реализации более дружественного интерфейса мне понадобится еще несколько вещей:
• Доступность создания;
• Доступность удаления;
• Список всех выделенных исключений, для массового удаления.
Преобразовав все это в код, я получил интерфейс представления:
Вся формализация контроллера заключается в том, что он должен принимать менеджер данных и собственно само представление, которое поддерживает интерфейс, представленный выше. Преобразовав в код, получил:
В данном контроллере для проверки параметров я использую контракты, но для статьи это не имеет никакого значения.
Создавая и формализуя можно позабыть о том, зачем все это затевалось. Все ради одного, повысить качество работы за счет получения адекватного критерия оценки завершенности работ. Поэтому сразу же начал формализовать правила в тесты. Для реализации интерфейсов, принимаемых контроллером в конструктор реализуем просто стабы.
При этом должна быть доступна кнопка создания исключений, недоступна кнопка удаления исключений, элемент в фокусе должен быть null, а список выбранных элементов пустой, но не null. Преобразовав в код получил:
После чего, формализуя остальные правила, я получил список тестов, который сразу же позволял оценить готовность функционала контроллера к работе. Ни разу не запустив самого клиента.
![](https://habrastorage.org/r/w780q1/files/d68/526/ed7/d68526ed769f4d6d8602f355faf98ec4.jpg)
После этого я создал само представление, реализующее вышеуказанный интерфейс. Для него я так же создал небольшие тесты, используя забавный класс PrivateObject, который позволяет вызывать приватные методы, реализованные в объекте. Например, тест на удаление я получил такой:
После чего формализуя остальные правила, я получил список тестов, который сразу же позволял оценить готовность функционала представления к работе. Опять же, ни разу не запустив самого клиента. Постепенно реализуя каждый из кусочков функционала я добился картины:
![](https://habrastorage.org/r/w780q1/files/752/353/557/75235355789a4039999b0b00cb53c7cd.jpg)
После, сделал вызов в коде контроллера с уже реальным менеджером и представлением.
Запустил все приложение и увидел конечный результат работы. Все работало точно так, как я закладывал. Отдел тестирования доволен, клиенты довольны, положительная обратная связь получена. Что еще необходимо?
![](https://habrastorage.org/r/w780q1/files/308/ab8/2f6/308ab82f6a4d4ffab37a43ac9c4d7f32.jpg)
Очень буду рад любым замечаниям или своим рассказам о том, как вы справляетесь с Windows Forms. Первые отзывы правильно замечают, что своим пыхтением я пришел к MVP, это абсолютно верно.
Спасибо за внимание! Надеюсь, вы не зря потратили время на меня.
Не будет:
• Классического Unit Test;
• Принижения влияния Code Behind;
• Неожиданных открытий в MVC.
Будет:
• Unit Test и Stub;
• MVC;
• Внедрение подхода на базе существующего продукта.
Введение
Перед тем, как начать использовать данных подход, я прошел большой путь обычного разработчика корпоративных систем. По пути мне встречались абсолютно шаблонные события и вещи, как простые, так и более сложные. Например, разработка закончена, функциональность отправлена на тестирование, кликается, далее валится масса ошибок, от простых недочетов, до грубых нарушений. Более сложный случай: функциональность принята, пользователи активно участвуют в обратной связи и приводят к необходимости менять функционал. Функционал поменяли, пошли по тому же пути и перестал работать старый функционал, да и новый оставляет желать лучшего. Кое как прищёлкивая быстрорастущую функциональность начинается постоянная правка багов. Сначала это были часы, потом недели, сейчас я знаю, что и месяцы не предел. Когда это занимало часы или дни, ошибки закрывались, все более – менее текло. Но стоило начать переоткрывать ошибки – начинался ад. Отдел тестирования постоянно ругается, разработчики постоянно правят ошибки и делают новые в разрабатываемом функционале. Все чаще приходит обратная связь, от реальных пользователей, полная негатива. А функционал рос и рос, я искал методы и подходы, как можно себе упростить жизнь. При этом меня давно посещала идея, задействовать при разработке пользовательского интерфейса тестирование на уровне разработчика. Желание заложить пользовательские сценарии привело меня к осознанию факта наличия проблемы: отсутствия хоть сколько-то объективных критериев готовности продукта от разработчика. И я начал мыслить несколько шире, а что, если постараться заложить все положительные сценарии использования в некоторые правила, постоянно проверять наборы правил, ведь это хоть какая-то гарантия того, что пользователь на следующем выпуске не будет ошарашен отвалившимся функционалом, а тестировщики будут задействованы по назначению. Именно этим правилам и предрекалось стать критерием окончания работы над функционалом для себя, для разработчика.
Формализация
Начались поиски инструментов, для формализации правил. Весь код логики взаимодействия с пользователем и данными находился в формах. Конечно же, были отделены слои работы с базой данных, сервисы, ремоутинги и многое, многое другое. Но пользовательский интерфейс оставался перенасыщен логикой. Логикой, никак не связанной с отображением. Первым, и вполне логичным шагом, мне показалось использовать средства для визуального тестирования форм. Набираем правило их кликов – вводов и преобразуем это в тест. Для примера это прекрасно умеет делать Visual Studio 2013 Ultimate, вернее один из её шаблонов. Я бодро создал несколько тестов и понял, что создавать таким образом тесты, а потом запускать, огромная беда. Требовалось кучу времени чтобы полностью загрузить всю систему, дойти до нужных диалогов и вызвать соответствующие события. Это требовало работу всех звеньев цепи, что местами было невозможно. Все действия я предпринимал абсолютно отделив для себя Unit Testing, при этом его постоянно использовал для сервисов и логики, которая вращалась на сервере, ведь у неё не было форм, соответственно Code Behind. Параллельно работая с ASP.NET MVC мне все больше нравился подход с отделением логики от представлений. Упрощенно, вызывая контроллер ASP.NET MVC мы просто дергаем его публичный метод, который возвращает представление с некоторыми изменениями. Я начал думать, как эту модель переложить на WinForms и пришел к выводу, что используя события на представлениях я буду вызывать методы контроллеров. Я принял правило, что у меня всегда есть класс-контроллер, который привязывается к событиям класса, реализующего интерфейс представления. Тем самым я убрал весь код логики из Code Behind в код контроллера, а контроллер подлежал тестированию с помощью обычных Unit Tests, реализуя Stubs для классов менеджеров данных и самих представлений. Позже я научился тестировать и сами представления, исключительно с помощью Unit Tests. Далее я хочу показать пример того, как я пришел к этому.
Пример
Описание задачи
Все описанное выше я рассмотрю на примере реализации формы изменения исключений при импорте данных. Исключения это просто сущность с тремя полями, необходимая для чтения текстовых файлов в системе. С точки зрения пользователя данная форма должна позволять добавлять, изменять, удалять исключения. После добавления нового исключения оно должно добавляться в конец списка. При удалении исключения фокус должен переходить на первый элемент списка. При этом форма вызывается из контекста никак не реализующего MVC.
Формализация представления
Воспитанный на огромном количестве форм, в корпоративных приложениях, я пришел к выводу, что тоже буду использовать стандартный подход к интерфейсу: панель управления с кнопками — действиями над исключениями, список исключений, в виде таблицы, и базовой кнопкой закрыть. Соответственно для отображения в интерфейсе мне понадобится:
• Весь список исключений, для построения таблицы;
• Исключение в фокусе;
• Событие смены исключения в фокусе;
• Событие добавления;
• Событие удаления;
• Событие нажатия кнопки закрыть;
• Метод открытия;
• Метод закрытия;
• Метод обновления представления.
В дальнейшем я понял, что для реализации более дружественного интерфейса мне понадобится еще несколько вещей:
• Доступность создания;
• Доступность удаления;
• Список всех выделенных исключений, для массового удаления.
Преобразовав все это в код, я получил интерфейс представления:
public interface IReplaceView
{
bool EnableCreate
{
get;
set;
}
bool EnableDelete
{
get;
set;
}
List<ReplaceSymbol> Replaces
{
set;
}
ReplaceSymbol FocusedReplace
{
get;
set;
}
List<ReplaceSymbol> SelectedReplaces
{
get;
set;
}
event EventHandler Closing;
event EventHandler Creating;
event EventHandler Deleting;
event EventHandler FocusedChanged;
void Open();
void Close();
void RefreshView();
}
Формализация контроллера
Вся формализация контроллера заключается в том, что он должен принимать менеджер данных и собственно само представление, которое поддерживает интерфейс, представленный выше. Преобразовав в код, получил:
public class ReplaceController : IReplaceController
{
private IReplaceView _view = null;
private IReplaceStorage _replaceStorage = null;
public ReplaceController(IReplaceView view, IReplaceStorage replaceStorage)
{
Contract.Requires(view != null, "View can’t be null");
Contract.Requires(replaceStorage != null, "ReplaceStorage can’t be null”);
_view = view;
_replaceStorage = replaceStorage;
}
}
В данном контроллере для проверки параметров я использую контракты, но для статьи это не имеет никакого значения.
Тестирование
Создавая и формализуя можно позабыть о том, зачем все это затевалось. Все ради одного, повысить качество работы за счет получения адекватного критерия оценки завершенности работ. Поэтому сразу же начал формализовать правила в тесты. Для реализации интерфейсов, принимаемых контроллером в конструктор реализуем просто стабы.
Загрузить пустой список исключений
При этом должна быть доступна кнопка создания исключений, недоступна кнопка удаления исключений, элемент в фокусе должен быть null, а список выбранных элементов пустой, но не null. Преобразовав в код получил:
[TestMethod]
public void LoadEmptyReplaces()
{
var emptyReplaces = new List<ReplaceSymbol>();
var storage = new StubIReplaceStorage()
{
ReplaceSymbolsGet =
() =>
{
return emptyReplaces;
},
};
ReplaceSymbol focusedReplace = null;
var loadedReplaces = new List<ReplaceSymbol>();
var selectedReplaces = new List<ReplaceSymbol>();
var enableCreate = false;
var enableDelete = false;
var view = new StubIReplaceView()
{
FocusedReplaceGet =
() =>
{
return focusedReplace;
},
FocusedReplaceSetReplaceSymbol =
x =>
{
focusedReplace = x;
if (x != null)
{
selectedReplaces.Add(x);
}
},
ReplacesSetListOfReplaceSymbol =
x =>
{
loadedReplaces = x;
},
EnableCreateSetBoolean =
x =>
{
enableCreate = x;
},
EnableDeleteSetBoolean =
x =>
{
enableDelete = x;
},
SelectedReplacesGet =
() =>
{
return selectedReplaces;
},
};
var controller = new ReplaceController(view, storage);
controller.Open();
view.FocusedChangedEvent(null, null);
Assert.IsNotNull(loadedReplaces, "После создания контроллераи и его открытия список замен не должен быть null");
Assert.IsTrue(loadedReplaces.Count == 0, "После создания контроллера и его открытия список замен не должен быть пустым, так как в него загружен пустой список");
Assert.IsNull(focusedReplace, "После создания контроллера и его открытия активной замены быть не должно");
Assert.IsNotNull(selectedReplaces, "После создания контроллера список замен не может быть null");
Assert.IsTrue(selectedReplaces.Count == 0, "После создания контроллера и его открытия в список выбраных замен должен быть пустой");
Assert.IsTrue(enableCreate, "После создания контроллера должна быть доступна возможность создавать замены");
Assert.IsFalse(enableDelete, "После создания контроллера не должно быть доступно удаление, так как список замен пустой");
}
Другие правила
После чего, формализуя остальные правила, я получил список тестов, который сразу же позволял оценить готовность функционала контроллера к работе. Ни разу не запустив самого клиента.
![](https://habrastorage.org/files/d68/526/ed7/d68526ed769f4d6d8602f355faf98ec4.jpg)
Реализация представления
После этого я создал само представление, реализующее вышеуказанный интерфейс. Для него я так же создал небольшие тесты, используя забавный класс PrivateObject, который позволяет вызывать приватные методы, реализованные в объекте. Например, тест на удаление я получил такой:
[TestMethod]
public void DeleteOneReplaceView()
{
var replaces = GetTestReplaces();
var storage = new StubIReplaceStorage()
{
ReplaceSymbolsGet =
() =>
{
return replaces;
},
};
var controller = new ReplaceController(_view, storage);
Task.Run(() => { controller.Open(); }).Wait(WaitTaskComplited);
var privateObject = new PrivateObject(_view);
privateObject.Invoke("RiseDeleting");
Assert.IsTrue(replaces.Count == 2, "После удаления одной замены должны остаться еще две");
Assert.IsNotNull(_view.FocusedReplace, "После удаления замены фокус должен перейти на первую замену");
Assert.AreEqual(replaces.First().Source, _view.FocusedReplace.Source, "После удаления замены фокус должен перейти на первую замену из оставшегося списка");
Assert.IsTrue(_view.SelectedReplaces.Count == 1, "После удаления замены должна быть выбрана первая из оставшегося списка");
Assert.AreEqual(_view.FocusedReplace.Source, _view.SelectedReplaces.First().Source, "После удаления замены фокус должен перейти на первую замену из оставшегося списка");
Assert.IsTrue(_view.EnableCreate, "После удаления замены должна быть доступна возможность добавлять замены");
Assert.IsTrue(_view.EnableDelete, "После удаления замены должна быть возможность удалить первую из оставшегося списка");
}
После чего формализуя остальные правила, я получил список тестов, который сразу же позволял оценить готовность функционала представления к работе. Опять же, ни разу не запустив самого клиента. Постепенно реализуя каждый из кусочков функционала я добился картины:
![](https://habrastorage.org/files/752/353/557/75235355789a4039999b0b00cb53c7cd.jpg)
После, сделал вызов в коде контроллера с уже реальным менеджером и представлением.
_replaceView = new ReplaceView();
_replaceController = new ReplaceController(_replaceView, _importer);
Запустил все приложение и увидел конечный результат работы. Все работало точно так, как я закладывал. Отдел тестирования доволен, клиенты довольны, положительная обратная связь получена. Что еще необходимо?
![](https://habrastorage.org/files/308/ab8/2f6/308ab82f6a4d4ffab37a43ac9c4d7f32.jpg)
Очень буду рад любым замечаниям или своим рассказам о том, как вы справляетесь с Windows Forms. Первые отзывы правильно замечают, что своим пыхтением я пришел к MVP, это абсолютно верно.
Спасибо за внимание! Надеюсь, вы не зря потратили время на меня.