Wednesday, August 24, 2022
HomeWeb DevelopmentUnit testing Flutter code with Mockito

Unit testing Flutter code with Mockito


Unit testing verifies if a single methodology or class works as anticipated. It additionally improves maintainability by confirming if present logic nonetheless works when new modifications are made.

Usually, unit checks are straightforward to put in writing however run in a check setting. This, by default, produces an empty response with standing code 400 when a community name or HTTP request is made. To repair this, we will simply use Mockito to return a faux response anytime we make an HTTP request. Mockito has varied use instances that we’ll introduce regularly as we proceed.

On this tutorial, we’ll exhibit tips on how to use Mockito to check Flutter code. We’ll discover ways to generate mocks, stub knowledge, and carry out checks on strategies that emit a stream. Let’s get began!

What’s Mockito?

Mockito is a widely known bundle that makes it simpler to generate a faux implementation of an present class. It eliminates the stress of repeatedly writing these functionalities. Moreover, Mockito helps management the enter so we will check for an anticipated consequence.

Utilizing Mockito hypothetically makes writing unit checks simpler, nevertheless, with dangerous structure, mocking and writing unit checks can simply get difficult.

Later on this tutorial, we’ll find out how Mockito is used with the model-view-viewmodel (MVVM) sample that includes separating the codebase into totally different testable components, just like the view mannequin and repositories.

Producing mocks and stubbing knowledge

Mocks are faux implementations of actual courses. They’re usually used to regulate the anticipated results of a check or when actual courses are susceptible to errors in a check setting.

To grasp this higher, we’ll write unit checks for an software that offers with sending and receiving posts.

Undertaking construction overview

Earlier than we begin, let’s add all the mandatory packages to our mission.

dependencies:
  dio: ^4.0.6 # For making HTTP requests

dev_dependencies:
  build_runner: ^2.2.0 # For producing code (Mocks, and many others)
  mockito: ^5.2.0 # For mocking and stubbing

We’ll use the MVVM and repository sample that’ll embrace checks for each the repositories and look at fashions. In Flutter, it’s a superb follow to position all of the checks within the check folder and it carefully match the construction of the lib folder.

Subsequent, we’ll create the authentication_repository.dart and authentication_repository_test.dart information by appending _test to the file names. This helps the check runner discover all of the checks current within the mission.

Test Files

We’ll begin this part by creating a category referred to as AuthRepository. Because the identify implies, this class will deal with all of the authentication performance in our app. After that, we’ll embrace a login methodology that checks if the standing code equals 200 and catches any errors that happen whereas authenticating.

class AuthRepository {
  Dio dio = Dio();

  AuthRepository();

  Future<bool> login({
    required String e-mail,
    required String password,
  }) async {
    attempt {
      last consequence = await dio.submit(
        '<https://reqres.in/api/login>',
        knowledge: {'e-mail': e-mail, 'password': password},
      );

      if (consequence.statusCode != 200) {
        return false;
      }
    } on DioError catch (e) {
      print(e.message);
      return false;
    }

    return true;
  }

  // ...
}
void essential() {
  late AuthRepository authRepository;

  setUp(() {
    authRepository = AuthRepository();
  });

  check('Efficiently logged in person', () async {
    count on(
      await authRepository.login(e-mail: '[email protected]', password: '123456'),
      true,
    );
  });
}

Within the check above, we initialize the AuthRepository within the setup operate. Since it can run earlier than each check and check group straight inside essential, it can initialize a brand new auth repository for each check or group.

Subsequent, we’ll write a check that expects the login methodology to return true with out throwing errors. Nevertheless, the check nonetheless fails as a result of unit testing doesn’t assist making a community request by default, therefore why the login request made with Dio returns a standing code 400.

Status Code 400 Error

To repair this, we will use Mockito to generate a mock class that features like Dio. In Mockito, we generate mocks by including the annotation @GenerateMocks([classes]) to the start of the essential methodology. This informs the construct runner to generate mocks for all of the courses within the checklist.


Extra nice articles from LogRocket:


@GenerateMocks([Dio, OtherClass])
void essential(){
    // check for login
}

Subsequent, open the terminal and run the command flutter pub run build_runner construct to start out producing mocks for the courses. After code technology is full, we’ll be capable to entry the generated mocks by prepending Mock to the category identify.

@GenerateMocks([Dio])
void essential(){
      MockDio mockDio = MockDio()
      late AuthRepository authRepository;
      ...
}

We now have to stub the information to ensure MockDio returns the appropriate response knowledge once we name the login endpoint. In Flutter, stubbing means returning a faux object when the mock methodology known as. For instance, when the check calls the login endpoint utilizing MockDio, we should always return a response object with standing code 200.

Stubbing a mock may be carried out with the operate when(), which can be utilized with thenReturn, thenAnswer, or thenThrow to supply the values wanted once we name the mock methodology. The thenAnswer operate is used for strategies that return a future or stream, whereas thenReturn is used for regular, synchronous strategies of the mock class.

// To stub any methodology; offers error when used for futures or stream
when(mock.methodology()).thenReturn(worth);

// To stub methodology that return a future or stream
when(mock.methodology()).thenAnswer(() => futureOrStream);

// To stub error
when(mock.methodology()).thenThrow(errorObject);

// dart
@GenerateMocks([Dio])
void essential() {
  MockDio mockDio = MockDio();
  late AuthRepository authRepository;

  setUp(() {
    authRepository = AuthRepository();
  });

  check('Efficiently logged in person', () async {
    // Stubbing
    when(mockDio.submit(
      '<https://reqres.in/api/login>',
      knowledge: {'e-mail': '[email protected]', 'password': '123456'},
    )).thenAnswer(
      (inv) => Future.worth(Response(
        statusCode: 200,
        knowledge: {'token': 'ASjwweiBE'},
        requestOptions: RequestOptions(path: '<https://reqres.in/api/login>'),
      )),
    );

    count on(
      await authRepository.login(e-mail: '[email protected]', password: '123456'),
      true,
    );
  });
}

After creating our stub, we nonetheless have to cross in MockDio into the check file so it will get used as an alternative of the true dio class. To implement this, we’ll take away the definition or instantiation of the true dio class from the authRepository and permit it to be handed by its constructor. This idea known as dependency injection.

Dependency injection

Dependency injection in Flutter is a method by which one object or class provides the dependencies of one other object. This sample ensures that the check and look at fashions can each outline the kind of dio they wish to use.

class AuthenticationRepository{
        Dio dio;

        // As a substitute of specifying the kind of dio for use
        // we let the check or viewmodel outline it
        AuthenticationRepository(this.dio)
}

@GenerateMocks([Dio])
void essential() {
  MockDio mockDio = MockDio();
  late AuthRepository authRepository;

  setUp(() {
    // we will now cross in Dio as an argument
    authRepository = AuthRepository(mockDio);
  });
}

Utilizing argument matchers

Within the earlier login instance, if the e-mail [email protected] was modified to [email protected] when making the request, the check would produce a no stub discovered error. It’s because we solely created the stub for [email protected].

Nevertheless, typically, we wish to keep away from duplicating pointless logic through the use of argument matchers offered by Mockito. With argument matchers, we will use the identical stub for a variety of values as an alternative of the precise kind.

To grasp matching arguments higher, we’ll check for the PostViewModel and create mocks for the PostRepository. Utilizing this strategy is really helpful as a result of once we stub, we’ll be returning customized objects or fashions as an alternative of responses and maps. It’s additionally very straightforward!

First, we’ll create the PostModel to extra cleanly characterize the information.

class PostModel {
  PostModel({
    required this.id,
    required this.userId,
    required this.physique,
    required this.title,
  });

  last int id;
  last String userId;
  last String physique;
  last String title;

  // implement fromJson and toJson strategies for this
}

Subsequent, we create the PostViewModel. That is used to retrieve or ship knowledge to the PostRepository. PostViewModel isn’t doing something extra than simply sending and retrieving knowledge from the repository and notifying the UI to rebuild with the brand new knowledge.

import 'bundle:flutter/materials.dart';
import 'bundle:mockito_article/fashions/post_model.dart';
import 'bundle:mockito_article/repositories/post_repository.dart';

class PostViewModel extends ChangeNotifier {
  PostRepository postRepository;
  bool isLoading = false;

  last Map<int, PostModel> postMap = {};

  PostViewModel(this.postRepository);

  Future<void> sharePost({
    required int userId,
    required String title,
    required String physique,
  }) async {
    isLoading = true;
    await postRepository.sharePost(
      userId: userId,
      title: title,
      physique: physique,
    );

    isLoading = false;
    notifyListeners();
  }

  Future<void> updatePost({
    required int userId,
    required int postId,
    required String physique,
  }) async {
    isLoading = true;
    await postRepository.updatePost(postId, physique);

    isLoading = false;
    notifyListeners();
  }

  Future<void> deletePost(int id) async {
    isLoading = true;
    await postRepository.deletePost(id);

    isLoading = false;
    notifyListeners();
  }

  Future<void> getAllPosts() async {
    isLoading = true;
    last postList = await postRepository.getAllPosts();

    for (var submit in postList) {
      postMap[post.id] = submit;
    }

    isLoading = false;
    notifyListeners();
  }
}

As acknowledged earlier, we mock the dependencies and never the precise courses we check for. On this instance, we write unit checks for the PostViewModel and mock the PostRepository. This implies we’d name the strategies within the generated MockPostRepository class as an alternative of the PostRepositorywhich may presumably throw an error.

Mockito makes matching arguments very straightforward. As an example, check out the updatePost methodology within the PostViewModel. It calls the repository updatePost methodology, which accepts solely two positional arguments. For stubbing this class methodology, we will select to supply the precise postId and physique, or we will use the variable any offered by Mockito to maintain issues easy.

@GenerateMocks([PostRepository])
void essential() {
  MockPostRepository mockPostRepository = MockPostRepository();
  late PostViewModel postViewModel;

  setUp(() {
    postViewModel = PostViewModel(mockPostRepository);
  });

  check('Up to date submit efficiently', () {
    // stubbing with argument matchers and 'any'
    when(
      mockPostRepository.updatePost(any, argThat(incorporates('stub'))),
    ).thenAnswer(
      (inv) => Future.worth(),
    );

    // This methodology calls the mockPostRepository replace methodology
    postViewModel.updatePost(
      userId: 1,
      postId: 3,
      physique: 'embrace `stub` to obtain the stub',
    );

    // confirm the mock repository was referred to as
    confirm(mockPostRepository.updatePost(3, 'embrace `stub` to obtain the stub'));
  });
}

The stub above consists of each the any variable and the argThat(matcher) operate. In Dart, matchers are used to specify check expectations. We now have various kinds of matchers appropriate for various check instances. For instance, the matcher incorporates(worth) returns true if the item incorporates the respective worth.

Matching positional and named arguments

In Dart, we even have each positional arguments and named arguments. Within the instance above, the mock and stub for the updatePost methodology offers with a positional argument and makes use of the any variable.

Nevertheless, named arguments don’t assist the any variable as a result of Dart doesn’t present a mechanism to know if a component is used as a named argument or not. As a substitute, we use the anyNamed(’identify’) operate when coping with named arguments.

when(
  mockPostRepository.sharePost(
    physique: argThat(startsWith('stub'), named: 'physique'),
    postId: anyNamed('postId'),
    title: anyNamed('title'),
    userId: 3,
  ),
).thenAnswer(
  (inv) => Future.worth(),
);

When utilizing matchers with a named argument, we should present the identify of the argument to keep away from an error. You may learn extra on matchers within the Dart documentation to see all potential obtainable choices.

Creating fakes in Mockito

Mocks and fakes are sometimes confused, so let’s shortly make clear the distinction between the 2.

Mocks are generated courses that permit stubbing through the use of argument matchers. Fakes, nevertheless, are courses that override the present strategies of the true class to supply extra flexibility, all with out utilizing argument matchers.

For instance, utilizing fakes as an alternative of mocks within the submit repository would permit us to make the faux repository operate much like the true one. That is potential as a result of we’d be capable to return outcomes based mostly on the values offered. In easier phrases, once we name sharePost within the check, we will select to avoid wasting the submit and later verify if the submit was saved utilizing getAllPosts.

class FakePostRepository extends Faux implements PostRepository {
  Map<int, PostModel> fakePostStore = {};

  @override
  Future<PostModel> sharePost({
    int? postId,
    required int userId,
    required String title,
    required String physique,
  }) async {
    last submit = PostModel(
      id: postId ?? 0,
      userId: userId,
      physique: physique,
      title: title,
    );
    fakePostStore[postId ?? 0] = submit;
    return submit;
  }

  @override
  Future<void> updatePost(int postId, String physique) async {
    fakePostStore[postId] = fakePostStore[postId]!.copyWith(physique: physique);
  }

  @override
  Future<Record<PostModel>> getAllPosts() async {
    return fakePostStore.values.toList();
  }

  @override
  Future<bool> deletePost(int id) async {
    fakePostStore.take away(id);

    return true;
  }
}

The up to date check utilizing faux is proven under. With faux, we will check for all of the strategies directly. A submit will get is to the map within the repository when it’s added or shared.

@GenerateMocks([PostRepository])
void essential() {
  FakePostRepository fakePostRepository = FakePostRepository();
  late PostViewModel postViewModel;

  setUp(() {
    postViewModel = PostViewModel(fakePostRepository);
  });

  check('Up to date submit efficiently', () async {
    count on(postViewModel.postMap.isEmpty, true);
    const postId = 123;

    postViewModel.sharePost(
      postId: postId,
      userId: 1,
      title: 'First Publish',
      physique: 'My first submit',
    );
    await postViewModel.getAllPosts();
    count on(postViewModel.postMap[postId]?.physique, 'My first submit');

    postViewModel.updatePost(
      postId: postId,
      userId: 1,
      physique: 'My up to date submit',
    );
    await postViewModel.getAllPosts();
    count on(postViewModel.postMap[postId]?.physique, 'My up to date submit');
  });
}

Updated Post Successfully Screen

Mocking and testing streams in Flutter

Mocking and stubbing streams with Mockito are similar to futures as a result of we use the identical syntax for stubbing. Nevertheless, streams are fairly totally different from futures as a result of they supply a mechanism to repeatedly hear for values as they’re emitted.

To check for a technique that returns a stream, we will both check if the tactic was referred to as or verify if the values are emitted in the appropriate order.

class PostViewModel extends ChangeNotifier {
  ...
  PostRepository postRepository;
  last likesStreamController = StreamController<int>();

  PostViewModel(this.postRepository);

  ...
  void listenForLikes(int postId) {
    postRepository.listenForLikes(postId).hear((likes) {
      likesStreamController.add(likes);
    });
  }
}


@GenerateMocks([PostRepository])
void essential() {
  MockPostRepository mockPostRepository = MockPostRepository();
  late PostViewModel postViewModel;

  setUp(() {
    postViewModel = PostViewModel(mockPostRepository);
  });

  check('Hear for likes works accurately', () {
    last mocklikesStreamController = StreamController<int>();

    when(mockPostRepository.listenForLikes(any))
        .thenAnswer((inv) => mocklikesStreamController.stream);

    postViewModel.listenForLikes(1);

    mocklikesStreamController.add(3);
    mocklikesStreamController.add(5);
    mocklikesStreamController.add(9);

    // checks if hear for likes known as
    confirm(mockPostRepository.listenForLikes(1));
    count on(postViewModel.likesStreamController.stream, emitsInOrder([3, 5, 9]));
  });
}

Within the instance above, we added a listenforLikes methodology that calls the PostRepository methodology and returns a stream that we will take heed to. Subsequent, we created a check that listens to the stream and checks if the tactic known as and emitted in the appropriate order.

For some complicated instances, we will use expectLater or expectAsync1 as an alternative of utilizing solely the count on operate.

Conclusion

So simple as most of this logic appears to be like, it’s crucial to put in writing checks so we don’t repeatedly QA these functionalities. One of many goals of writing checks is to cut back repetitive QA as your apps get bigger.

On this article, we realized how we will successfully use Mockito for producing mocks whereas writing unit checks. We additionally realized tips on how to use fakes and argument matchers to put in writing purposeful checks.

Hopefully, you’ve gotten a greater understanding of tips on how to construction your software to make mocking simpler. Thanks for studying!

: Full visibility into your net and cell apps

LogRocket is a frontend software monitoring answer that permits you to replay issues as in the event that they occurred in your personal browser. As a substitute of guessing why errors occur, or asking customers for screenshots and log dumps, LogRocket allows you to replay the session to shortly perceive what went flawed. It really works completely with any app, no matter framework, and has plugins to log further context from Redux, Vuex, and @ngrx/retailer.

Along with logging Redux actions and state, LogRocket data console logs, JavaScript errors, stacktraces, community requests/responses with headers + our bodies, browser metadata, and customized logs. It additionally devices the DOM to document the HTML and CSS on the web page, recreating pixel-perfect movies of even essentially the most complicated single-page net and cell apps.

.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments