Тестирование

Тестирование - неотъемлемая часть всего процесса разработки, которая служит повышению качества вашего ПО. Хорошо настроенная система тестирования экономит усилия вашего отдела сопровождения и улучшает имидж всей компании.

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

Сначала тесты пишутся и отлаживаются локально, а затем развёртываются и запускаются на билд-сервере автоматически. Если тесты не проходят, считается, что не прошёл и билд. Тем самым, исключается возможность выхода ПО, не проходящего тесты.

Система тестов строится на основе написанных интерфейсов, задающих форму реализуемых компонентов (модулей), и тестовых классов, вызывающих методы этих интерфейсов и проверяющих их работу. Написание юнит-тестов базируется (и будет нормально работать) на двух основных подходах - модульной архитектуре и Dependency Injection (DI). Каждый модуль может тестироваться отдельно, при этом зависимости внедряются в него как раз при помощи DI.

Поэтому для функционирования всей вашей системы тестирования придётся переработать архитектуру ваших систем. Необходимо избавиться от статических классов бизнес-логики и ликвидировать жёсткие зависимости между вашими компонентами. Зависимости должны передаваться в качестве аргументов конструкторов и методов. Этим вы и обеспечите работу DI в вашем решении.

В качестве фреймворка для поддержки DI можно использовать любой. Мы использовали Unity.

Можно выделить три основных уровня тестирования:

  • юнит-тестирование, при котором проверяется один модуль; его зависимости внедряются через конструктор;
  • интеграционное тестирование, при котором тестируется система работающих модулей или приложение целиком; в данном случае зависимости от внешней среды также подставляются при помощи DI;
  • системное тестирование: проверяется совокупность совместно работающих приложений с реальными зависимостями. Для подобных тестов лучше всего подходят контейнеры типа Docker.

Юнит-тестирование предполагает проверку отдельных методов; интеграционное и системное тестирование предполагают наличие сценариев, в которых проверяется состояние системы после вызова ряда методов. Тестирование сценариев может также происходить и в режиме нагрузки (нагрузочное тестирование).

Могу дать полезный совет: необязательно изготавливать заглушки (mock-объекты) для всех модулей приложения. Юнит- и интеграционное тестирование можно проводить по индукции.

 modules.png

Допустим, что модуль A зависит от модуля B, а модуль B - от внешнего компонента C. В том случае при тестировании B вы вместо C используете заглушку и проверяете работоспособность всех функций B.

После этого при тестировании модуля A вам уже необязательно создавать заглушку для B. Вы проверили, что B работает корректно, и можете использовать при тестировании A непосредственно его, а не заглушку. И далее, по индукции, вы продолжаете тестировать модули, зависящие от A, уже доказав работоспособность A.

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

Как только вы покроете функционал тестами, вы увидите, насколько легко станет выполнять рефакторинг - тесты будут вас поддерживать, не давая сломать работающую систему. Скорость рефакторинга и общая скорость вашей работы возрастут.

Test-driven development

Test-driven development - "разработка, управляемая тестированием". Этот подход к разработке позволяет задавать требования к программным компонентам на формальном языке. Фактически, тесты задают спецификацию разрабатываемого ПО. И это гораздо более удобно для программиста, нежели чтение пространной текстовой документации.

Внедять TDD следует на двух фронтах: на исправлении текущих ошибок и на разработке новой функциональности.

При исправлении текущих ошибок мы, прежде чем править код, описываем ошибочное поведение нашего ПО и убеждаемся, что тест не проходит. Далее мы исправляем ошибку и уже точно знаем, что она более не произойдёт - ведь теперь у нас есть поддержка в виде написанного теста.

При разработки нового функционала действует общий алгоритм. Разработка ведётся циклами (не путать с итерациями; циклы - это Epics в терминологии JIRA), и каждый цикл включает в себя один и тот же набор шагов:

  1. Выделяем набор функциональности, который мы хотим реализовать. Создаём и вычленяем из ТЗ текстовое описание этой функциональности.
  2. Проектируем API, соответствующее этой функциональности.
  3. Описываем API в виде интерфейсов.
  4. Описываем требования к API в виде текста.
  5. Переводим текстовые требования в программный код тестов.
  6. Убеждаемся, что написанные тесты не проходят.
  7. Реализуем заявленное API.
  8. Убеждаемся, что тесты проходят. Если это не так, возвращаемся к шагу 7.
  9. Выполняем рефакторинг. Это необходимый шаг, чтобы код был универсальным, а не просто описывал реакцию на определённый набор тестовых данных.

Если вашем приложение основано на архитектуре MVVM, то проектировать лучше сначала только ViewModel. По прохождении всех тестов VM можно заняться проектированием и реализацией View. Тесты для View - вещь довольно тонкая, и мы в этом плане использовали исключительно ручное тестирование.

По сути дела, в процесс работы в рамках концепции TDD на каждом шаге требования к ПО постепенно переводятся от текстового во всё более формализованное представление, и всё это завершается написанием исходного кода.

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

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

В рамках одной итерации никто не мешает выполнить несколько этапов одного цикла (если успеваете). Некоторые работы хорошо параллелятся.

Всё описанное выше можно оформить в виде следующей таблицы разработки одного цикла.

Итерация

Этап

Исполнитель

Выход

Комментарий

0

[Проектирование:]

Разработка ТЗ

Аналитик

Часть ТЗ (касаемо реализуемой функциональности)

Выдержка из ТЗ. Текстовое описание функционала и пользовательских сценариев

1

Согласование ТЗ

Менеджер проекта, аналитик, архитектор, проектировщик UX

При необходимости в ТЗ вносятся правки

2

Разработка архитектуры

Архитектор

Архитектура ПО

Функциональность описывается текстом в виде набора объектов, свойств и операций. Для каждой операции описываются входные и выходные параметры и изменения, которое операция осуществляет с системой

3

Разработка спецификации

Архитектор

Спецификация ПО

Тоже описывается текстом, но теперь используются идентификаторы классов, интерфейсов и функций. Параметрам задаются имена и типы данных

Создание описания тестов

Тестировщик

Описание тестов

Сценарии взаимодействия переводятся в текстовое описание отдельных тестов, оперирующих понятиями разработанной архитектуры (применяются указанные в архитектуре операции)

Проектирование интерфейса

Проектировщик UX

Макет интерфейса

На основании объектов, свойств и операций проектируется макет пользовательского интерфейса

4

Декларирование спецификации

Программист

Исходный код

На основании спецификации создаются интерфейсы и необходимые типы данных

5

Написание тестов

Тестировщик

Исходный код

Текстовое описание тестов переводится в код при помощи декларированных интерфейсов

8

[Разработка:]

Разработка

Программист

Исходный код

Реализует функционал, проверяет прохождение тестов

9

Рефакторинг

Программист

Исходный код

Шлифовка кода, проверка прохождения тестов

10

Реализация пользовательского интерфейса

Программист

Пользовательской интерфейс

На основе макета

11

Стилизация пользовательского интерфейса

Дизайнер

Таблица стилей

Ручное тестирование

Тестировщик

12

Релиз

По завершении этапов при необходимости можно проводить совещания. По завершении всего цикла разработки можно провести большую демонстрацию.

Некоторые этапы можно пропускать. В частности, архитектуру и спецификацию можно объединить.

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

Далее