Faking vs. Mocking

Hello, Amigos! This is Pavel Gershevich, Mobile Team Lead of the Amiga product development agency. We have already covered more than half of the guide on testing in Flutter! Today article translation is dedicated to the Faking technique. And in the following parts we will consider common mistakes and best practices in writing Unit tests. So stay tuned!

New releases, useful plugins and libraries, cases and personal experience in our author's telegram channel Flutter. Many. Our community already has 2645 mobile developers, join us!

Faking

What will happen in the example from previous articleif you add a parameter of type BuildContext to the function 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) {}
}

Then, you need to update the test like this:

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);
    });
  });
}

But BuildContext – abstract class, then how can it be initialized?

At this stage, it is necessary to create a new fake type by extending the class Fake.

class FakeBuildContext extends Fake implements BuildContext {}

Instead of creating a real object BuildContext()you only need to create a fake object FakeBuildContext().

registerFallbackValue(FakeBuildContext());

...

loginViewModel.login(FakeBuildContext(), email);

Full source code

However, if you inherit from a class Mock instead of Fakethen the test will still pass. So what is the difference between Fake And Mock?

Faking vs. Mocking

The terms Fake and Mock are called “Test Doubles”. Test doubles are objects that replace real ones during testing. In other words, both techniques are used to create fake classes and objects. They are also used to simulate the methods of fake objects and to control the return values ​​of these methods.

While the Mocking technique uses Stubbing to simulate and control the result of functions, it cannot be used with Faking. Faking allows you to override the methods of a real class in the form needed for testing.

Let's write tests for the LoginViewModel class again from parts 3but we will use the Faking technique instead of Mocking.

First, let's create a class FakeSharedPreferenceswhich is inherited from Fake. If you use Mock, you need to replace the methods getString And clearbut when using Faking they need to be overridden.

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);
  }
}

Next, we will need to remove the lines of code that use 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);
});

However, when you run the test, it will crash.

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);
});

This happened because of a function override. clear for return Future.value(true)but is expected to return Future.value(false). So there is no need to use the class FakeSharedPreferences to test this test case. Instead, let's create a new class to override the function clearso that she would return 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);
  }
}

Then for the failing test shown above we will use the class SecondFakeSharedPreferences.

final fakeSharedPreferences = SecondFakeSharedPreferences();

Full source code

You can see when using Faking, you can create multiple classes Faketo achieve this. This is the disadvantage of using Faking. What are the advantages of Faking?

To understand the benefits of Faking, let's move on to another example. Let's say there are classes JobViewModel, JobRepository And 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();
  }
}

This is class 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;
    }
  }
}

Now let's write a test for it.

First you need to create a class FakeJobRepository. Let's create a variable jobDataInDb with type List<JobData> to simulate real data in the Isar database. Then you can override all 4 methods in 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;
  }
}

Next we will test the function addJob in class 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",
    });
  });
});

More test cases can be found Here.

Thus, not only individual functions by type were tested getAllJobs And addJobbut also test cases where these functions work together. This helps make testing more similar to running in a real environment.

If you use Mocking and Stubbing to test a function addJobthen the code will look like this:

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);
});

With this approach it is not determined whether the function works correctly addJob or not. Alternatively, you can write the code like this:

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",
  });
});

When it was replaced by 4 JobDatathen, definitely, the result in the expression expect there will also be 4 JobData. Therefore, there is no need to determine whether the function works correctly. addJob or not.

To sum it up, using Faking can be more effective than Mocking in cases like this.

In the next article we will look at the most common mistakes in testing.

Subscribe to telegram channel Flutter. Manyso you don't miss the new issue and much more interesting information about cross-platform development.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *