реклама
разместить

Полный гайд по тестированию на Flutter. Часть 6: Тестовые двойники: Faking vs Mocking

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

Полный гайд по тестированию на Flutter. Часть 6: Тестовые двойники: Faking vs Mocking

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

Полный гайд по тестированию на Flutter. Часть 6: Тестовые двойники: Faking vs Mocking

Faking

Что произойдет в примере из прошлой статьи, если добавить параметр с типом BuildContext в функцию push?

import 'package:flutter/material.dart'; class LoginViewModel { final Navigator navigator; LoginViewModel({ required this.navigator, }); void login(BuildContext context, String email) { if (email.isNotEmpty) { navigator.push(context, 'home'); } } } class Navigator { void push(BuildContext context, String name) {} }

Тогда, нужно обновить тест таким образом:

void main() { ... setUpAll(() { registerFallbackValue(BuildContext()); }); ... group('login', () { test('navigator.push should be called once when the email is not empty', () { ... loginViewModel.login(BuildContext(), email); verify(() => mockNavigator.push(any(), any())).called(1); }); }); }

Но BuildContext — абстрактный класс, тогда как можно его инициализировать?

На этом этапе необходимо создать новый фейковый тип, расширив класс Fake.

class FakeBuildContext extends Fake implements BuildContext {}

Вместо создания реального объекта BuildContext(), нужно создать только фейковый объект FakeBuildContext().

registerFallbackValue(FakeBuildContext()); ... loginViewModel.login(FakeBuildContext(), email);

Однако, если наследоваться от класса Mock вместо Fake, то тест все равно пройдет. Так в чем же разница между Fake и Mock?

Faking vs Mocking

Термины Fake и Mock называют «Тестовыми двойниками». Тестовые двойники — объекты, которые заменяют реальные во время тестирования. Другими словами, обе техники используются для создания фейковых классов и объектов. Также они применяются для имитации методов фейковых объектов и для контроля возвращаемых значений этими методами.

Если техника Mocking использует Stubbing для имитации и контроля результата функций, то с Faking её применять нельзя. Faking позволяет переопределять методы реального класса в нужном для тестирования виде.

Давайте снова напишем тесты для класса LoginViewModel из части 3, но будем использовать технику Faking вместо Mocking.

Сначала создадим класс FakeSharedPreferences, который наследуется от Fake. Если использовать Mock, то необходимо подменить методы getString и clear, но при использовании Faking их нужно переопределить.

class FakeSharedPreferences extends Fake implements SharedPreferences { @override String? getString(String key) { if (key == 'ntminh@gmail.com') { return '123456'; } return null; } @override Future<bool> clear() { return Future.value(true); } }

Далее, нам нужно будет убрать строки кода, которые используют Stubbing.

test('login should return false when the password are incorrect', () { // Arrange final fakeSharedPreferences = FakeSharedPreferences(); final loginViewModel = LoginViewModel( sharedPreferences: fakeSharedPreferences, ); String email = 'ntminh@gmail.com'; String password = 'abc'; // Stubbing -> remove this line // when(() => mockSharedPreferences.getString(email)).thenReturn('123456'); // Act final result = loginViewModel.login(email, password); // Assert expect(result, false); });

Однако, при запуске теста, он упадет.

test('logout should throw an exception when the clear method returns false', () async { // Arrange final fakeSharedPreferences = FakeSharedPreferences(); final loginViewModel = LoginViewModel( sharedPreferences: fakeSharedPreferences, ); // Stubbing -> remove this line // when(() => mockSharedPreferences.clear()) // .thenAnswer((_) => Future.value(false)); // Act final Future<bool> Function() call = loginViewModel.logout; // Assert expect(call, throwsFlutterError); });

Это произошло из-за переопределения функции clear для возвращения Future.value(true), но ожидается, что вернется Future.value(false). Поэтому не нужно использовать класс FakeSharedPreferences для проверки этого тест кейса. Вместо этого создадим новый класс, чтобы переопределить функцию clear, чтобы она возвращала Future.value(false).

class SecondFakeSharedPreferences extends Fake implements SharedPreferences { @override String? getString(String key) { if (key == 'ntminh@gmail.com') { return '123456'; } return null; } @override Future<bool> clear() { return Future.value(false); } }

Тогда для падающего теста, показанного выше, будем использовать класс SecondFakeSharedPreferences.

final fakeSharedPreferences = SecondFakeSharedPreferences();

Можно заметить, когда используется Faking, можно создать несколько классов Fake, чтобы достичь этого. Это недостаток использования Faking. А какие есть плюсы у Faking?

Чтобы узнать преимущества Faking, давайте перейдем к другому примеру. Допустим, есть классы JobViewModel, JobRepository и JobData:

class JobRepository { final Isar isar; JobRepository({required this.isar}); Future<void> addJob(JobData jobData) async { await isar.writeTxn(() async { isar.jobDatas.put(jobData); }); } Future<void> updateJob(JobData jobData) async { await isar.writeTxn(() async { isar.jobDatas.put(jobData); }); } Future<void> deleteJob(int id) async { await isar.writeTxn(() async { isar.jobDatas.delete(id); }); } Future<List<JobData>> getAllJobs() async { return await isar.jobDatas.where().findAll(); } }

Это класс JobViewModel.

class JobViewModel { JobRepository jobRepository; JobViewModel({required this.jobRepository}); final Map<int, JobData> jobMap = {}; Future<void> addJob({ required JobData jobData, }) async { await jobRepository.addJob(jobData); } Future<void> updateJob({ required JobData jobData, }) async { await jobRepository.updateJob(jobData); } Future<void> deleteJob(int id) async { await jobRepository.deleteJob(id); } Future<void> getAllJobs() async { final jobs = await jobRepository.getAllJobs(); jobMap.clear(); for (var post in jobs) { jobMap[post.id] = post; } } }

Теперь напишем тест для него.

Сначала необходимо создать класс FakeJobRepository. Создадим переменную jobDataInDb с типом List для имитации реальных данных в базе данных Isar. Тогда можно переопределить все 4 метода в JobRepository.

class FakeJobRepository extends Fake implements JobRepository { // Suppose initially there are 3 jobs in the database. final jobDataInDb = [ JobData()..id = 1..title = 'Job 1', JobData()..id = 2..title = 'Job 2', JobData()..id = 3..title = 'Job 3', ]; @override Future<void> addJob(JobData jobData) async { jobDataInDb.add(jobData); } @override Future<void> updateJob(JobData jobData) async { jobDataInDb .firstWhere((element) => element.id == jobData.id) .title = jobData.title; } @override Future<void> deleteJob(int id) async { jobDataInDb.removeWhere((element) => element.id == id); } @override Future<List<JobData>> getAllJobs() async { return jobDataInDb; } }

Далее протестируем функцию addJob в классе JobViewModel.

group('addJob', () { test('should add job to jobMap', () async { // before adding job await jobViewModel.getAllJobs(); expect(jobViewModel.jobMap, { 1: JobData()..id = 1..title = 'Job 1', 2: JobData()..id = 2..title = 'Job 2', 3: JobData()..id = 3..title = 'Job 3', }); await jobViewModel .addJob(jobData: JobData()..id = 4..title = 'Job 4'); // after adding job await jobViewModel.getAllJobs(); expect(jobViewModel.jobMap, { 1: JobData()..id = 1..title = 'Job 1', 2: JobData()..id = 2..title = 'Job 2', 3: JobData()..id = 3..title = 'Job 3', 4: JobData()..id = 4..title = 'Job 4', }); }); });

Больше тест кейсов можно найти здесь.

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

Если использовать Mocking и Stubbing для тестирования функции addJob, то код будет выглядеть так:

test('should add job to jobMap', () async { // Arrange final jobData = JobData()..id = 4..title = 'Job 4'; // Stub when(() => mockJobRepository.addJob(jobData)) .thenAnswer((_) async {}); // Act await jobViewModel.addJob(jobData: jobData); // Assert verify(() => mockJobRepository.addJob(jobData)).called(1); });

При таком подходе не определяется, корректно работает функция addJob или нет. Альтернативно можно написать код так:

test('should add job to jobMap', () async { final jobData = JobData()..id = 4..title = 'Job 4'; // Stub when(() => mockJobRepository.addJob(jobData)) .thenAnswer((_) async {}); when(() => mockJobRepository.getAllJobs()).thenAnswer((_) async { return [ JobData()..id = 1..title = 'Job 1', JobData()..id = 2..title = 'Job 2', JobData()..id = 3..title = 'Job 3', JobData()..id = 4..title = 'Job 4', ]; }); // Act await jobViewModel.addJob(jobData: jobData); await jobViewModel.getAllJobs(); // Assert expect(jobViewModel.jobMap, { 1: JobData()..id = 1..title = 'Job 1', 2: JobData()..id = 2..title = 'Job 2', 3: JobData()..id = 3..title = 'Job 3', 4: JobData()..id = 4..title = 'Job 4', }); });

Когда заменили на 4 JobData, то, определенно, результат в выражении expect будет тоже 4 JobData. Поэтому не нужно определять корректно работает функция addJob или нет.

Подводя итоги, использование Faking может быть более эффективно, чем Mocking, в случаях, похожих на этот.

В следующей статье рассмотрим наиболее часто встречающиеся ошибки в тестировании.

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

22
реклама
разместить
1 комментарий

Название техники забавное) Всего одна буква и название становится описанием тестирования, когда в нем что-то идет не так 😂

Как создать приложение для iOS

Всем привет! На связи Илья Прудников, CEO Beetrail. Разработка мобильных приложений для iOS продолжает стремительно расти, и цифры это только подтверждают. На октябрь 2024 года в App Store можно найти около 6 миллионов сервисов – это огромный выбор для пользователей и отличная возможность для разработчиков. Каждую неделю магазин посещает 656 миллио…

Как создать приложение для iOS
22
реклама
разместить
Личный опыт использования технологии GPT

Добрый день, я Евгений Николаевич Рычков, и я хочу поделиться результатами использования Giga Chat. Затрагивать эту тему я начал чуть ли не в пионерской стадии развития, когда хайпа еще не было. Вот, например, с Воробьевым Александром и без него выступал в Точке Кипения на тему кодинга с использованием GPT

11

💡 «Делимобиль» представил финансовые результаты по МСФО за 2024 год.

Мобильные тесты за пару кликов с помощью Maestro?
Мобильные тесты за пару кликов с помощью Maestro?
11
Научились делать отличный результат в любых условиях и выпустили заметные релизы

Привет! Это Коля, CEO студии GRCH. Мы разрабатываем, поддерживаем и масштабируем цифровые продукты. В статье поделюсь нашим опытом за 9 лет и расскажу, как научились делать отличный результат в любых условиях.

Научились делать отличный результат в любых условиях и выпустили заметные релизы
44
Код пишется быстро, а баги дорого: как на самом деле оценить стоимость фичи
Код пишется быстро, а баги дорого: как на самом деле оценить стоимость фичи
Кассовая дисциплина для малого бизнеса в 2025 году: избегаем ошибок и штрафов

Здравствуйте! Меня зовут Алексей Авдюшкин. Сегодня ведение малого бизнеса требует не только предпринимательской хватки, но и четкого понимания финансовой дисциплины. Особенно это касается работы с наличными деньгами и кассовыми операциями. Я подготовил подробное руководство, которое поможет разобраться в тонкостях кассовой дисциплины и избежать рас…

Кассовая дисциплина для малого бизнеса в 2025 году: избегаем ошибок и штрафов
Область нажатия в Figma
Обложка

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

33
11
[]