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

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

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

По сути мы сегодня поупражняемся в применении инверсии зависимостей в сценариях, где требуется какой-либо ввод от пользователей. Чтобы сильно не нагружать эту статью базовыми концепциями модульного тестирования и тестовых дублеров, а также информацией о библиотеках для вышеперечисленного, я предполагаю что вы в этом уже разбираетесь. Если же вам не хватает познаний в какой либо из этих тем, я предлагаю вам самим сделать небольшой экскурс и ознакомиться с этими концепциями. Хорошей базой может послужить техническая документация по модульному тестированию за моим авторством на LinkedIn. В этой же статье внимание уделяется тому, как применять концепции модульного тестирования и тестовых дублеров конкретно в контексте кода, который требует пользовательского ввода.

Подход и инструментарий

Примеры, представленные в этой статье, основаны на очень простом XAML/WPF-решении, реализующем паттерн MVVM (Model, View, View Model). Паттерн MVVM был выбран из-за своей естественной пригодности для модульного тестирования. Тем не менее, одна лишь реализация этого паттерна сама по себе не гарантирует, что весь ваш код тоже будет пригоден для модульного тестирования.

В нашем примере используется фреймворк MVVM-Light и IoC-контейнер Unity (IoC - Inversion of Control). В рамках этой статьи я использую IoC-контейнер MVVM-Light и расширение Common Service Locator от Unity. Несмотря на использование этих инструментов, несмотря на то, что мы реализуем MVVM, мы не можем проводить модульное тестирование наших функциональных средств. Функциональные средства в нашем случае представляют из себя простую форму с кнопкой, которая предлагает пользователю обновить заголовок формы текущим временем и датой, как показано на рисунке 1:

Рисунок 1 : Если пользователь нажимает кнопку “yes” в появившемся окне, заголовок формы обновляется, отражая текущую дату и время.
Рисунок 1 : Если пользователь нажимает кнопку “yes” в появившемся окне, заголовок формы обновляется, отражая текущую дату и время.

Рисунок 2 демонстрирует XAML и код модели представления, который мы хотим сделать пригодным для модульного тестирования:

Рисунок 2 : В паттерне MVVM функционал представления размещается в модели представления, которая привязывается к конкретным элементам представления.
Рисунок 2 : В паттерне MVVM функционал представления размещается в модели представления, которая привязывается к конкретным элементам представления.

То, как сейчас размещен код ButtonClick, препятствует модульному тестированию, поскольку мы не можем изолировать функцию от реализации окна сообщения. Другими словами, мы наблюдаем жесткую зависимость от окна сообщения. Важно отметить, что IoC-контейнер Unity наше приложение использует в качестве способа реализации MVVM. Рисунок 3 иллюстрирует, как IoC Unity вписывается в эту картину:

Рисунок 3 : Свойство DataContext нашего представления гидратируется инстансом DialogViewModel, получаемым из IoC-контейнера.
Рисунок 3 : Свойство DataContext нашего представления гидратируется инстансом DialogViewModel, получаемым из IoC-контейнера.

IoC-контейнер не является обязательным элементом для реализации MVVM. Я добавил его сюда как часть реализации MVVM, чтобы проиллюстрировать, что одно лишь присутствие IoC-контейнера, который является воплощением принципа инверсии зависимостей, само по себе не означает, что ваше приложение будет пригодным для модульного тестирования. MVVM позволяет нам избежать прямой реализации кода в пользовательском интерфейсе. Это также называется “отделенный код” (code behind).

Вопрос, который часто встает перед командой, которая размышляет о модульном тестировании, заключается в том, где провести черту. Другими словами, какие аспекты вашего приложения не стоит даже рассматривать в качестве кандидатов для модульного тестирования? На мой взгляд, все, что связано с фреймворками, не стоит покрывать модульными тестами. В данном случае мы говорим про MVVM-Light, Unity и сам .NET Framework. Предположительно, проекты с исходным кодом этих элементов уже покрыты такими тестами. Я бы никогда не стал тратить время на проверку того, как работает MessageBox.Show. Это элемент фреймворка, который, по моему предположению, должен работать. Это предположение исключает необходимость в модульном тестировании. Модульное тестирование вступает в игру там, где вы не можете сделать такое предположение. В данном случае меня не интересует, работает ли MessageBox. Скорее меня интересует, как метод ButtonClick из моей модели представления ведет себя в ответ на MessageBox.

Когда речь заходит о модульном тестировании, нам не обойтись без специальных инструментов, среди которых стоят особняком библиотека для модульного тестирования MSTest и библиотека для мокинга Moq.

Первая версия метода ButtonClick в нашей модели представления

На рисунке 4 показано изначальное состояние метода ButtonClick:

Рисунок 4 : Метод ButtonClick не подлежит модульному тестированию, поскольку он напрямую зависит от MessageBox.
Рисунок 4 : Метод ButtonClick не подлежит модульному тестированию, поскольку он напрямую зависит от MessageBox.

Хорошая новость заключается в том, что ButtonClick работает. Плохая новость заключается в том, что у нас нет возможности отдельно проверить, работает ли он, не запуская приложение. В этом коде есть две серьезные проблемы и одна небольшая. Первая серьезная проблема — прямая зависимость от DateTime.Now в конструкторе. Второй серьезной проблемой является прямая зависимость ButtonClick от MessageBox. Третьей проблемой является несоблюдение ButtonClick принципа разделения обязанностей. Одним словом, это не чистый код. В следующем разделе мы сделаем его лучше.

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

Модульное тестирование позволяет устранять незначительные недоработки до того, как они перерастут в серьезные проблемы. И не все эти проблемы носят технический характер. Одна из наиболее серьезных проблем для программного обеспечения — недоверие пользователя. Ваши конечные пользователи должны верить, что программное обеспечение работает так, как вы заявляете. Те же самые конечные пользователи должны быть уверены, что вы способны предоставить такое программное обеспечение.

С функциональной точки зрения спецификация метода ButtonClick (в контексте пользовательского интерфейса) такова: когда пользователь нажимает кнопку “Yes” на всплывающем окне, заголовок формы обновляется текущим значением даты и времени.

Хотя это и достаточно тривиальный сценарий для иллюстрации, конечные пользователи нажимают кнопки и ожидают результата все время. Вопрос в том, как мы можем проверить функцию ButtonClick в автоматизированном модульном тесте. Ответ заключается в рефакторинге кода таким образом, чтобы применить инверсию зависимостей к модели представления. У нас есть средство для упрощения инверсии зависимостей — Unity Inversion of Control Container.

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

Вторая версия метода ButtonClick в нашей модели представления

На рисунке 5 показана улучшенная версия нашей модели представления.

Рисунок 5 : Удаление прямых зависимостей от DateTime.Now и MessageBox делает DialogViewModel пригодным для модульного тестирования.
Рисунок 5 : Удаление прямых зависимостей от DateTime.Now и MessageBox делает DialogViewModel пригодным для модульного тестирования.

Модель представления больше не имеет прямой зависимости от DateTime.Now и MessageBox. Благодаря внедрению зависимостей, которому способствует IoC-контейнер, как показано на рисунке 6, теперь у вас есть четкое разделение обязанностей, которое делает ButtonClick пригодным для модульного тестирования. Абстрагируя функционал MessageBox от модели представления, я также воспользовался возможностью упростить интерфейс. Модель представления больше не должна зависеть от классов MessageBoxButtons и DialogResult.

Рисунок 6 : Для автоматического внедрения зависимостей при создании модели представления нам необходимо внести соответствующие строчки в контейнер.
Рисунок 6 : Для автоматического внедрения зависимостей при создании модели представления нам необходимо внести соответствующие строчки в контейнер.

Кроме того, поскольку модель представления больше не имеет прямой зависимости от DateTime.Now, мы можем подставить в “сейчас” (now) любое значение даты и времени, какое захотим. В модульном тестировании нам необходима возможность установить данные в определенное желаемое состояние. Если у вас когда-либо были модульные тесты, выполняющие математические операции с датами, которые внезапно начинали непроходиться после того, как уже некоторое время находились в эксплуатации, проверьте, нет ли DateTime.Now в тестируемом коде или в самом тесте.

ButtonClick больше не содержит отдельного вызова RaisePropertyChange для уведомления WPF об обновлении привязок. В качестве альтернативы свойство WindowCaption получило новый сеттер для выполнения этой задачи. Теперь ButtonClick делает одну единственную вещь: обновляет свойство WindowCaption, если Show возвращает True, что, в свою очередь, происходит только в том случае, если пользователь нажимает кнопку “Yes”.

На рисунке 7 показан новый класс DialogService.

Рисунок 7 : DialogService реализует интерфейс с одним методом Show, который можно мокнуть в рамках модульного тестирования.
Рисунок 7 : DialogService реализует интерфейс с одним методом Show, который можно мокнуть в рамках модульного тестирования.

На рисунке 8 показан новый класс ClockService.

Рисунок 8 : ClockService реализует интерфейс с одним методом методом Now, который можно мокнуть в рамках модульного тестирования.
Рисунок 8 : ClockService реализует интерфейс с одним методом методом Now, который можно мокнуть в рамках модульного тестирования.

MVVM Framework и Unity работают вместе, чтобы автоматически произвести гидратацию IoC-контейнера и найти ресурсы, необходимые для запуска приложения. Возвращаясь к рисунку 3, загрузка ресурсов происходит в вызове ServiceLocator.Current.GetInstance<DialogViewModel>();. GetInstance — это фабричный метод, выполняющий работу по бутстреппингу модели представления. Этот процесс бутстреппинга включает пинг IoC-контейнера для получения необходимых ресурсов.

Создание самого модульного теста

Рисунок 9 иллюстрирует модульный тест.

Рисунок 9 : Модульный тест проверяет ожидаемый результат для каждого сценария ответа пользователя.
Рисунок 9 : Модульный тест проверяет ожидаемый результат для каждого сценария ответа пользователя.

Поскольку зависимости модели представления манифестированы как сервисы, реализующие интерфейсы, вы можете использовать мокинг-фреймворк для создания тестовых дублеров, которые, в свою очередь, внедряются в конструктор модели представления. С помощью тестовых дублеров вы можете контролировать поведение методов. Что касается тестируемой системы, внедренные объекты являются реальными. Касательно сервиса-часов, последовательность подготовки используется для поддержки двух вызовов. Это необходимо, поскольку реализация имеет два вызова: один в конструкторе модели представления, который всегда вызывается, и один в ButtonClick, который может быть вызван в зависимости от того, нажимает ли пользователь кнопку “Yes” в MessageBox .

В целях тестирования нет ни интерактивного пользователя, ни фактического MessageBox. Что касается модели представления, то она получает True или False в ответ от сервиса. Чтобы облегчить модульный тест, мок-объект настроен на возврат True или False. Поведение модели представления изменяется в зависимости от того какое значение возвращается. Поскольку мы знаем, что вернет мок Clock, из этого следует, что мы можем проверить, правильно ли ведет себя ButtonClick, когда метод Show возвращает True или False.

Заключение

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

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

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


Приглашаем всех желающих на открытое занятие «Коллекции и структуры данных», которое проведет Станислав Шурупин, Lead Software Engineer. На вебинаре мы рассмотрим основные универсальные коллекции .NET (Array, List, Dictionary, Queue, Stack, Hashtable и другие более специфичные), обсудим их назначение, реализацию, методы, производительность, а также как делать выбор в пользу той или иной структуры. Регистрация доступна по ссылке.

Комментарии (1)


  1. yerbabuena
    18.07.2022 10:15

    Как по мне, не описана гораздо более интересная задача - как протестировать созданный XAML. Зачастую в WPF там помещается почти вся необходимая функциональность.