Узнайте, как создать приложение Flutter с использованием подхода к разработке, основанному на тестировании. Привыкайте к TDD в ритме Red-Green-Refactor и никогда не бойтесь вносить изменения в свой код.

«Если вы хотите, чтобы ваши системы были гибкими, напишите тесты. Если вы хотите, чтобы ваши системы можно было использовать повторно, напишите тесты. Если вы хотите, чтобы ваши системы были обслуживаемыми, напишите тесты ».

- Дядя Боб Мартин

РАЗРАБОТКА С ТЕСТОВЫМИ УСЛОВИЯМИ

TDD - это процесс создания программного обеспечения путем предварительного написания тестов, чтобы доказать, что реализация соответствует требованиям. Для большинства разработчиков это не то, что приходит естественно, и самое сложное - к этому привыкнуть. Но хорошая новость заключается в том, что процесс TDD является ритмичным, и поэтому я постараюсь показать вам на примере, насколько легко это, когда вы чувствуете ритм. По сути, на протяжении всей вашей работы вам нужно повторять 3 шага / цвета:

  1. Напишите неудачный тест (красная фаза)
  2. Пройдите тест - напишите достаточно кода, чтобы пройти тест (Зеленая фаза)
  3. Улучшение кода - Убрать беспорядок (фаза рефакторинга)

АВТОМАТИЧЕСКИЕ ИСПЫТАНИЯ FLUTTER

У Flutter есть 3 категории тестов:

  1. Тест виджета (тестирует отдельный виджет)
  2. Модульный тест (тестирует одну функцию, метод или класс)
  3. Интеграционный тест (тестирует все приложение или большую часть приложения)

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

Модульные и интеграционные тесты будут включены в следующую статью, так что следите за обновлениями!

Наша задача - реализовать простой UI. Мы хотим создать экран, включающий виджеты Text и Button.

Не беспокойтесь о простоте экрана, потому что наша цель - привыкнуть к ритму TDD. Давайте начнем.

Если вы открыли свой проект и начали с чего-то другого, вместо создания тестового файла, остановитесь на этом!

Прежде чем что-либо реализовывать, мы напишем первый тест, который проверит, создан ли наш экран вообще. Помните, Red-Green-Refactor.

══╡ КРАСНЫЙ: главная страница

testWidgets('home page is created', (WidgetTester tester) async {
  final testWidget = MaterialApp(
    home: HomePage(),
  );

  await tester.pumpWidget(testWidget);
  await tester.pumpAndSettle();
});

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

══╡ ЗЕЛЕНЫЙ: главная страница

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Если мы проверим наш тест, синтаксическая ошибка исчезнет. Запустите тест с командой flutter test и проверьте, находимся ли мы в зеленой зоне. Отлично, у нас получилось!

══╡ РЕФАКТОР: Главная страница

Поскольку рефакторинг не требуется, наш 1-й цикл «красный-зеленый-рефакторинг» завершен. Достаточно просто?

Теперь, когда у нас есть база, следующим шагом будет создание теста, который проверит, есть ли у нас виджет Text на нашем экране и правильный ли текст.

══╡ КРАСНЫЙ: текст

testWidgets('home page contains hello world text',
    (WidgetTester tester) async {
  final testWidget = MaterialApp(
    home: HomePage(),
  );

  await tester.pumpWidget(testWidget);
  await tester.pumpAndSettle();

  expect(find.text('Hello World!'), findsOneWidget);
});

Если вы запустите команду flutter test, среда тестирования Flutter перехватит исключение:

The following TestFailure object was thrown running a test:
 Expected: exactly one matching node in the widget tree
 Actual: _TextFinder:<zero widgets with text “Hello World!” (ignoring offstage widgets)>
 Which: means none were found but one was expected

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

══╡ ЗЕЛЕНЫЙ: текст

Правило состоит в том, чтобы добавить достаточно кода, чтобы пройти тест.

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('Hello World!');
  }
}

Запустите flutter test, и мы успешно завершили зеленый шаг.

══╡ РЕФАКТОР: текст

Нам нужно это реорганизовать? Обычно да! Вероятно, вам нужно будет сделать что-то вроде добавления локализации, кода форматирования и т. Д. Но пока, поскольку это небольшой пример приложения и наш код отформатирован, мы готовы приступить к работе. Наш 2-й цикл рефакторинга красный-зеленый завершен!

Пришло время реализовать кнопку на нашей домашней странице.

Во-первых, нам нужно протестировать несколько вещей:

  • кнопка создана
  • отображается значок и текст
  • цвет фона синий
  • обратный вызов при нажатии правильно вызывается

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

══╡ КРАСНЫЙ: кнопка, значок, текст, цвет фона.

testWidgets('home page contains button', (WidgetTester tester) async {
  final testWidget = MaterialApp(
    home: HomePage(),
  );

  await tester.pumpWidget(testWidget);
  await tester.pumpAndSettle();

  final buttonMaterial = find.descendant(
    of: find.byType(ElevatedButton),
    matching: find.byType(Material),
  );

  final materialButton = tester.widget<Material>(buttonMaterial);

  expect(materialButton.color, Colors.blue);
  expect(find.text('Weather today'), findsOneWidget);
  expect(find.byKey(Key('icon_weather')), findsOneWidget);
});

Запустите flutter test, и среда тестирования Flutter перехватит исключение:

The following StateError was thrown running a test:
Bad state: No element

Создадим кнопку и попадем в зеленый рай.

══╡ ЗЕЛЕНЫЙ: кнопка, значок, текст, цвет фона.

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(children: [
      Text('Hello World!'),
      ElevatedButton(
          onPressed: () {},
          style: ElevatedButton.styleFrom(primary: Colors.blue),
          child: Row(children: [
            Icon(
              Icons.wb_sunny,
              key: Key('icon_weather'),
            ),
            Text('Weather today')
          ])),
    ]);
  }
}

Запускаем flutter test и… тесты пройдены! Пришло время для третьего шага.

══╡ РЕФАКТОР: кнопка, значок, текст, цвет фона

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

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Hello World!'),
        ElevatedButton(
          onPressed: () {},
          style: ElevatedButton.styleFrom(primary: Colors.blue),
          child: Row(
            children: [
              Icon(
                Icons.wb_sunny,
                key: Key('icon_weather'),
              ),
              Text('Weather today'),
            ],
          ),
        ),
      ],
    );
  }

Отформатируйте код и сделайте его красивым.

Давайте начнем с нашего последнего ритма "красный-зеленый-рефакторинг" для обратного вызова onPressed, и все готово! А может и нет?

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

══╡ КРАСНЫЙ: обратный вызов при нажатии вызывается правильно

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

testWidgets('notify when button is pressed', (WidgetTester tester) async {
  var pressed = false;
  final testWidget = MaterialApp(
    home: HomePage(
      onPressed: () => pressed = true,
    ),
  );

  await tester.pumpWidget(testWidget);
  await tester.pumpAndSettle();

  await tester.tap(find.byType(ElevatedButton));
  await tester.pumpAndSettle();

  expect(pressed, isTrue);
});

Полученная ошибка:

Error: No named parameter with the name ‘onPressed’.

Итак, давайте внесем некоторые изменения и пройдем этот тест.

══╡ ЗЕЛЕНЫЙ: обратный вызов при нажатии вызывается правильно

class HomePage extends StatelessWidget {
  const HomePage({Key key, this.onPressed}) : super(key: key);

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    ...
     ElevatedButton(
       onPressed: () => onPressed?.call(),
    ...
  }

Мы не знаем, является ли наш обратный вызов нулевым, поэтому, чтобы избежать ошибок, нам нужно добавить условный доступ к члену. Мы рады снова проверить наш тест! Пора еще раз запустить flutter test перед рефакторингом.

══╡ РЕФАКТОР: Обратный вызов при нажатии вызывается правильно

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

И у вас есть еще одна важная задача:

Запустите волшебную команду flutter test и наслаждайтесь временем в зеленой зоне!

Заключение

Создание приложений с использованием подхода TDD - определенно то, что поначалу кажется многим разработчикам странным и неестественным. Возможно, вы знаете и понимаете ритм Red-Green-Refactor, но почему-то все еще трудно к нему привыкнуть. Но со временем (и я надеюсь, что эта статья) вы будете больше доверять своему коду, и когда придет время для рефакторинга приложения, вы полюбите свои тесты!

До следующей статьи! Спасибо!

Код теста можно найти на Github здесь.