Hola, Amigos! На связи Павел Гершевич, Mobile Team Lead агентства разработки сайтов и мобильных приложений Amiga. Продолжаем нашу серию статей переводов о тестировании в Flutter. В этой и нескольких следующих частях поговорим о модульном (Unit) тестировании.

Перед тем, как приступить к разбору нескольких примеров хочу пригласить вас в наш авторский телеграмм-канал Flutter.Много. Мы с командой мобильных разработчиков Amiga рассказываем о личном опыте, делимся полезными плагинами\библиотеками, переводами статей и кейсами. Присоединяйтесь!

Простой Unit-тест

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

Используем две статичные функции.

class Validator {
static bool validateEmail(String value) {
return value.isNotEmpty;
}

static bool validatePassword(String value) {
return value.isNotEmpty;
}
}

Затем метод расширения под названием isNullOrEmpty.

extension StringExtension on String? {
bool get isNullOrEmpty => this == null || this!.isEmpty;
}

И наконец, функция входа.

import 'package:testing_examples/part2/ext/extension.dart';
import 'package:testing_examples/part2/util/utils.dart';

bool login(String? email, String? password) {
if (email.isNullOrEmpty || password.isNullOrEmpty) {
return false;
}
return Validator.validateEmail(email!) && Validator.validatePassword(password!);
}

Полный исходный код можно найти по ссылке: https://github.com/ntminhdn/testing_examples/tree/main/lib/part2

Необходимо протестировать 4 функции, которые находятся в 3 разных файлах, поэтому создаем 3 файла для тестов в папке test. Чтобы различать файлы unit-тестов и файлы, относящиеся к другим методам тестирования, таким как Widget-тесты, назовем папку unit_test внутри папки test.

Naming convention для файлов с тестами гласит, что для названия файла нужно использовать код и суффикс _test.dart. И еще одно правило — структура папки test должна повторять структуру папки lib, как показано в примере:

Сначала напишем тесты для функции validateEmail. Каждый файл должен начинаться с функции main() как точки входа. Для написания Unit-тестов нужно импортировать пакет flutter_test.

import 'package:flutter_test/flutter_test.dart';

void main() {
}

Для того, чтобы создать Unit-тест, используем функцию test, передавая ей 2 параметра: description и body.

void main() {
test('validateEmail should return true when the email is not empty', () {
// body
});
}

Не важно короткие или длинные наименования самих тестов. Главное, чтобы они были простыми для понимания без чтения кода.

Для тела теста обычно используется паттерн ААА: сначала, подготавливаем все необходимое (Arrange), потом выполняем нужное действие (Act) и проверяем его результат (Assert).

test('validateEmail should return true when the email is not empty', () {
// Arrange
String validEmail = 'test@example.com';

// Act
bool result = Validator.validateEmail(validEmail);

// Assert
expect(result, true);
});
  • Arrange — шаг, где создаем переменные и входные данные перед вызовом функции, которую хотим протестировать.

    Например, если нужно проверить функцию validateEmail когда email не пустой, то нужно создать переменную String validEmail = ‘test@example.com’.

  • Act — вызов функции, которую нужно протестировать с уже подготовленными на прошлом шаге входными данными: Validator.validateEmail(validEmail).

  • Assert — шаг, где проверяем соответствует ли результат ожиданиям, используя функцию expect.

    Например: expect(result, true), expect(result, 1000), expect(result, “Minh”), …

Итак, один Unit-тест описан. Теперь функция validateEmail должна быть протестирована еще для одного случая: когда email пустой, она должна вернуть false.

test('validateEmail should return false when the email is empty', () {
// Arrange
String invalidEmail = '';

// Act
bool result = Validator.validateEmail(invalidEmail);

// Assert
expect(result, false);
});

Напишем Unit-тесты для функции validatePassword в похожем виде.

test('validatePassword should return true when the password is not empty', () {
// Arrange
String validPassword = 'password123';

// Act
bool result = Validator.validatePassword(validPassword);

// Assert
expect(result, true);
});

test('validatePassword should return false when the password is empty', () {
// Arrange
String invalidPassword = '';

// Act
bool result = Validator.validatePassword(invalidPassword);

// Assert
expect(result, false);
});

Теперь в файле utils_test.dart есть 4 тестовых кейса. Нужно сгруппировать их по самим функциям, используя group.

group('validateEmail', () {
test('validateEmail should return true when the email is not empty', () {
// body
});

test('validateEmail should return false when the email is empty', () {
// body
});
});

group('validatePassword', () {
test('validatePassword should return true when the password is not empty',
// body
});

test('validatePassword should return false when the password is empty', () {
// body
});
});

Для того, чтобы запустить Unit-тесты, набираем в консоли команду flutter test или нажимаем Run или Debug в IDE, как на картинке ниже. Если в консоль выводится фраза «All tests passed!», то все тесты успешно прошли. Если какой-то из них провалится, то появится лог в консоли.

group('login', () {
test('login should return false when the email is empty', () {
// Arrange
String? email;
String password = 'password123';

// Act
bool result = login(email, password);

// Assert
expect(result, false);
});

test('login should return false when the password is empty', () {
// Arrange
String email = 'ntminh@gmail.vn';
String? password;

// Act
bool result = login(email, password);

// Assert
expect(result, false);
});

test('login should return false when the email and password are empty', () {
// Arrange
String? email;
String? password;

// Act
bool result = login(email, password);

// Assert
expect(result, false);
});

test('login should return true when the email and password are not empty',
() {
// Arrange
String email = 'ntminh@gmail.vn';
String password = 'password123';

// Act
bool result = login(email, password);

// Assert
expect(result, true);
});
});

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

Функция expect и Matcher

Функция expect используется для проверки соответствия результата условию (matcher).

expect(actual, matcher);

Matcher может быть булевым значением, например true, false, строковым значением, например «OK», или числом: 0, -1 и т. д. Также он может быть комплексным выражением, таким как:

  • isNull: используется для проверки, что текущее значение равно null.

  • isNotNull: используется для проверки, что текущее значение не равно null.

  • isTrue: одинаково со сравнением с true.

  • isFalse: одинаково со сравнением с false.

  • isList: используется для проверки, что текущее значение это List.

  • isMap: используется для проверки, что текущее значение это Map.

  • isA<T>(): используется для проверки, что текущее значение имеет тип T.

  • isException: используется для проверки, что текущее значение это Exception.

  • throwsArgumentError: используется для проверки, что во время выполнения кидает ArgumentError.

Мы познакомимся с большим количеством Matcher в следующих частях.

Заключение

В данной статье, мы написали простой Unit-тест. В следующих выпусках продолжим писать Unit-тесты для более сложных случаев с использованием продвинутых техник: Mock, Fake и Stub.

Подписывайтесь на телеграмм-канале Flutter. Много, чтобы не пропустить следующую статью!

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


  1. dushes_at_habr
    23.07.2024 12:35

    Чуть-чуть душноты )

    Очень важный момент вы сами указали – про использование матчеров.

    isTrue: одинаково со сравнением с true.

    Они повышают читаемость теста. И лучше помогают в разборе упавших тестов, когда на CI мы читаем логи, которые эти матчеры нам и будут выдавать.

    Но во всех тестах выше в expect вы используете проверку на true/false, что немного противоречит последнему абзацу с использованием матчеров.


    1. fognature1 Автор
      23.07.2024 12:35

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