Required

The following tools/software must be up and running :

Also Recommended

In order to work with code coverage, we will need lcov

The goal of this workshop is to take a deep dive into dart & flutter testing framework.

Flutter is booming in the field of cross-platform mobile application development. Whilst developers can create solid apps with Flutter, they also have to ensure that the existing functionality is not broken by the latest code changes.

In order to get that confidence, developers can write a layer of automated tests. It is also important to run the automated tests on a regular basis or on every code change to get continuous feedback, this is where continuous integration comes into the picture.

Get started

  1. If you have a github account, you can fork this project

  2. Checkout the working repository

     # Clone the original project or your github fork copy
     $ git clone https://github.com/bwnyasse/flutter_testing_tutorial.git
    
  1. Import the project in your editor

  2. Launch an emulator or connect a device to ensure that the application is running

     $ flutter run 
    
  3. Understand the goal of the project by reading the README.md

Application Architecture

What you'll learn

      _             _     _   _                          _      _       _     
  ___| |_ __ _ _ __| |_  | |_| |__   ___    ___ ___   __| | ___| | __ _| |__  
 / __| __/ _` | '__| __| | __| '_ \ / _ \  / __/ _ \ / _` |/ _ \ |/ _` | '_ \ 
 \__ \ || (_| | |  | |_  | |_| | | |  __/ | (_| (_) | (_| |  __/ | (_| | |_) |
 |___/\__\__,_|_|   \__|  \__|_| |_|\___|  \___\___/ \__,_|\___|_|\__,_|_.__/ 
                                                                                                        

Get started

    git checkout -f step1-TODO

Provide a sample response from the API

In order to mock a service making a http request, we must provide a sample response from the server.

Test of AppService in src/services/app_service.dart

1- Run the following command

    flutter test

2- We except an error like the following:

    00:01 +0 -1: loadMovies [E]                                                                                                                         
    UnimplementedError
    src/services/app_service_test.dart 5:5  main.<fn>
    
    00:01 +0 -1: Some tests failed. 

Initialize the AppService

1- Add the following lines of code in the app_service_test.dart#test('loadMovies'...

    final mockClient = MockClient((request) async {
    return Response(json.encode(exampleJsonResponse), 200);
    });
    final service = AppService(mockClient);

2- Ensure that all the import in the class are correct.

    import 'dart:convert';

    import 'package:flutter_movie_deep_dive_test/src/models/models.dart';
    import 'package:flutter_movie_deep_dive_test/src/services/services.dart';
    import 'package:flutter_test/flutter_test.dart';
    import 'package:http/http.dart';
    import 'package:http/testing.dart';

    import '../common.dart';

Fix the test('loadMovies') method

1 - Add the following lines of code in the app_service_test.dart#test('loadMovies'...

    final expectedResponse = MoviesResponse.fromJson(exampleJsonResponse);
    final actualResponse = await service.loadMovies();
    expect(actualResponse, equals(expectedResponse));

2- Run the following command

    flutter test

We except the following ouput:

    00:01 +1: All tests passed! 

What you'll learn

      _             _     _   _                          _      _       _     
  ___| |_ __ _ _ __| |_  | |_| |__   ___    ___ ___   __| | ___| | __ _| |__  
 / __| __/ _` | '__| __| | __| '_ \ / _ \  / __/ _ \ / _` |/ _ \ |/ _` | '_ \ 
 \__ \ || (_| | |  | |_  | |_| | | |  __/ | (_| (_) | (_| |  __/ | (_| | |_) |
 |___/\__\__,_|_|   \__|  \__|_| |_|\___|  \___\___/ \__,_|\___|_|\__,_|_.__/ 
                                                                                                        

Get started

    git checkout -f step2-TODO

1- Run the following command

    # Mac or Linux only 
    $ /bin/bash coverage.sh

or

    # Using Docker
    $ docker run --rm -it -v $PWD:/build/ bwnyasse/flutter-cirrusci-coverage

2- We except the following ouput:

Coverage 100% for AppService

We need to test the case status != 200 and ensure the way to catch the LoadMoviesException

1- Let's add the following method in the group('loadMovies')

    test('status != 200', () async {
            final mockClient = MockClient((request) async {
                    return Response(json.encode(exampleJsonResponse), 500);
            });
            final service = AppService(mockClient);
            expect(
                    () async => await service.loadMovies(),
                    throwsA(predicate((e) =>
                    e is LoadMoviesException &&
                    e.message == 'LoadMovies - Request Error: 500')),
            );
    });

2- Run the following command

    # Mac or Linux only 
    $ /bin/bash coverage.sh

or

    # Using Docker
    $ docker run --rm -it -v $PWD:/build/ bwnyasse/flutter-cirrusci-coverage

3- We except the following ouput:

What you'll learn

      _             _     _   _                          _      _       _     
  ___| |_ __ _ _ __| |_  | |_| |__   ___    ___ ___   __| | ___| | __ _| |__  
 / __| __/ _` | '__| __| | __| '_ \ / _ \  / __/ _ \ / _` |/ _ \ |/ _` | '_ \ 
 \__ \ || (_| | |  | |_  | |_| | | |  __/ | (_| (_) | (_| |  __/ | (_| | |_) |
 |___/\__\__,_|_|   \__|  \__|_| |_|\___|  \___\___/ \__,_|\___|_|\__,_|_.__/ 
                                                                                                        

Get started

    git checkout -f step3-TODO

1- Run the following command

    # Mac or Linux only 
    $ /bin/bash coverage.sh

or

    # Using Docker
    $ docker run --rm -it -v $PWD:/build/ bwnyasse/flutter-cirrusci-coverage

2- We except the following ouput:

Test posterPath is null

Add the following code in the test('posterPath is null'

  Movie m = Movie();
  expect(m.posterPathResolved, equals('https://via.placeholder.com/300'));

Test the posterPath is valid

Add the following code in the group('movie.posterPathResolved'

test('posterPath is valid', () {
  Movie m = Movie(posterPath: 'some-value');
  expect(m.posterPathResolved,
      equals('http://image.tmdb.org/t/p/w185/some-value'));
});

1- Run the following command

    # Mac or Linux only 
    $ /bin/bash coverage.sh

or

    # Using Docker
    $ docker run --rm -it -v $PWD:/build/ bwnyasse/flutter-cirrusci-coverage

2- We except the following ouput:

What you'll learn

      _             _     _   _                          _      _       _     
  ___| |_ __ _ _ __| |_  | |_| |__   ___    ___ ___   __| | ___| | __ _| |__  
 / __| __/ _` | '__| __| | __| '_ \ / _ \  / __/ _ \ / _` |/ _ \ |/ _` | '_ \ 
 \__ \ || (_| | |  | |_  | |_| | | |  __/ | (_| (_) | (_| |  __/ | (_| | |_) |
 |___/\__\__,_|_|   \__|  \__|_| |_|\___|  \___\___/ \__,_|\___|_|\__,_|_.__/ 
                                                                                                        

Get started

    git checkout -f step4-TODO

1- Run the following command

    # Mac or Linux only 
    $ /bin/bash coverage.sh

or

    # Using Docker
    $ docker run --rm -it -v $PWD:/build/ bwnyasse/flutter-cirrusci-coverage

2- We except the following ouput:

Provide a mockito mock of AppService

In order to initialize the bloc AppBloc , we need to mock the application service AppService

Initiliaze the test of the Application Bloc

1- Complete the test('close does not emit new app state'

We must use expectLater and emitsInOrder to match events from a stream. Add the following code

expectLater(
  appBloc,
  emitsInOrder([AppEmpty(), emitsDone]),
);

Cover the case for AppError and AppLoaded

1- Complete the test : group('AppState' with the following code :

test('AppError', () {
  when(serviceMock.loadMovies()).thenThrow(Error);

  final expectedResponse = [
    AppEmpty(),
    AppLoading(),
    AppError(),
  ];

  appBloc.add(FetchEvent());

  expectLater(
    appBloc,
    emitsInOrder(expectedResponse),
  );
});

test('AppLoaded', () {
  when(serviceMock.loadMovies()).thenAnswer((_) => Future.value(response));
  final expectedResponse = [
    AppEmpty(),
    AppLoading(),
    AppLoaded(response: response),
  ];

  appBloc.add(FetchEvent());

  expectLater(
    appBloc,
    emitsInOrder(expectedResponse),
  );
});

What you'll learn

      _             _     _   _                          _      _       _     
  ___| |_ __ _ _ __| |_  | |_| |__   ___    ___ ___   __| | ___| | __ _| |__  
 / __| __/ _` | '__| __| | __| '_ \ / _ \  / __/ _ \ / _` |/ _ \ |/ _` | '_ \ 
 \__ \ || (_| | |  | |_  | |_| | | |  __/ | (_| (_) | (_| |  __/ | (_| | |_) |
 |___/\__\__,_|_|   \__|  \__|_| |_|\___|  \___\___/ \__,_|\___|_|\__,_|_.__/ 
                                                                                                        

Get started

    git checkout -f step5-TODO

1- Run the following command

    # Mac or Linux only 
    $ /bin/bash coverage.sh

or

    # Using Docker
    $ docker run --rm -it -v $PWD:/build/ bwnyasse/flutter-cirrusci-coverage

2- We except the following ouput:

Use Finder to match the widget named MyHomePage

1- Complete the TODO with the following code :

  Finder textFinder = find.byType(MyHomePage);
  expect(textFinder, findsOneWidget);

2- Run the following command

    # Mac or Linux only 
    $ /bin/bash coverage.sh

or

    # Using Docker
    $ docker run --rm -it -v $PWD:/build/ bwnyasse/flutter-cirrusci-coverage

What you'll learn

      _             _     _   _                          _      _       _     
  ___| |_ __ _ _ __| |_  | |_| |__   ___    ___ ___   __| | ___| | __ _| |__  
 / __| __/ _` | '__| __| | __| '_ \ / _ \  / __/ _ \ / _` |/ _ \ |/ _` | '_ \ 
 \__ \ || (_| | |  | |_  | |_| | | |  __/ | (_| (_) | (_| |  __/ | (_| | |_) |
 |___/\__\__,_|_|   \__|  \__|_| |_|\___|  \___\___/ \__,_|\___|_|\__,_|_.__/ 
                                                                                                        

Get started

    git checkout -f step6-TODO

1- Run the following command

    flutter test

2- Fix the NetworkImageLoadException

You need to mock the way the application is loading the network url in the test.

Complete movie_card_test.dart#testWidgets('Display Movie Card'

What you'll learn

      _             _     _   _                          _      _       _     
  ___| |_ __ _ _ __| |_  | |_| |__   ___    ___ ___   __| | ___| | __ _| |__  
 / __| __/ _` | '__| __| | __| '_ \ / _ \  / __/ _ \ / _` |/ _ \ |/ _` | '_ \ 
 \__ \ || (_| | |  | |_  | |_| | | |  __/ | (_| (_) | (_| |  __/ | (_| | |_) |
 |___/\__\__,_|_|   \__|  \__|_| |_|\___|  \___\___/ \__,_|\___|_|\__,_|_.__/ 
                                                                                                        

Get started

    git checkout -f step7-TODO

1- Run the following command

    flutter test

What you'll learn

      _             _     _   _                          _      _       _     
  ___| |_ __ _ _ __| |_  | |_| |__   ___    ___ ___   __| | ___| | __ _| |__  
 / __| __/ _` | '__| __| | __| '_ \ / _ \  / __/ _ \ / _` |/ _ \ |/ _` | '_ \ 
 \__ \ || (_| | |  | |_  | |_| | | |  __/ | (_| (_) | (_| |  __/ | (_| | |_) |
 |___/\__\__,_|_|   \__|  \__|_| |_|\___|  \___\___/ \__,_|\___|_|\__,_|_.__/ 
                                                                                                        

Get started

    git checkout -f step8

1- Run the following command

    # Mac or Linux only 
    $ /bin/bash coverage.sh

or

    # Using Docker
    $ docker run --rm -it -v $PWD:/build/ bwnyasse/flutter-cirrusci-coverage

What you'll learn

      _             _     _   _                          _      _       _     
  ___| |_ __ _ _ __| |_  | |_| |__   ___    ___ ___   __| | ___| | __ _| |__  
 / __| __/ _` | '__| __| | __| '_ \ / _ \  / __/ _ \ / _` |/ _ \ |/ _` | '_ \ 
 \__ \ || (_| | |  | |_  | |_| | | |  __/ | (_| (_) | (_| |  __/ | (_| | |_) |
 |___/\__\__,_|_|   \__|  \__|_| |_|\___|  \___\___/ \__,_|\___|_|\__,_|_.__/ 
                                                                                                        

Get started

    git checkout -f step9-TODO
  1. The first file contains an “instrumented” version of the app. The instrumentation allows you to “drive” the app and record performance profiles from a test suite. This file can have any name that makes sense. For this example, create a file called test_driver/app.dart.

  2. The second file contains the test suite, which drives the app and verifies it works as expected. The test suite also records performance profiles. The name of the test file must correspond to the name of the file that contains the instrumented app, with _test added at the end. Therefore, create a second file called test_driver/app_test.dart.

Run integration tests

Now that you have an instrumented app and a test suite, run the tests. First, be sure to launch an Android Emulator, iOS Simulator, or connect your computer to a real iOS / Android device.

Then, run the following command from the root of the project:

    flutter drive --target=test_driver/app.dart

This command:

What you'll learn

      _             _     _   _                          _      _       _     
  ___| |_ __ _ _ __| |_  | |_| |__   ___    ___ ___   __| | ___| | __ _| |__  
 / __| __/ _` | '__| __| | __| '_ \ / _ \  / __/ _ \ / _` |/ _ \ |/ _` | '_ \ 
 \__ \ || (_| | |  | |_  | |_| | | |  __/ | (_| (_) | (_| |  __/ | (_| | |_) |
 |___/\__\__,_|_|   \__|  \__|_| |_|\___|  \___\___/ \__,_|\___|_|\__,_|_.__/ 
                                                                                                        

Example with github actions

Get started

    git checkout -f step10-TODO

With github actions, you can define the build pipeline of your application through many steps.

According to the documentation, let's have a look of a useful github actions files

    git checkout -f step10

You will see a the complete version of .github/workflows/ci.yml

Example with codecov.io

Well, it is nice to test your flutter code , but it is better to have to setup the code coverage.

     git checkout -f step11

You will see a the complete version of .github/workflows/ci.yml and .codecov.yamlto setup your code coverage with codecov.io

Example with codecov.io

If you want to use gitlab-ci,

    git checkout step-12

You will find my complete version of .gitlab-ci.yml. Use it as an inspiration for your pipeline. You may want to do things differently from my ci.

Example with CodeMagic CI/CD for Flutter

Let's have a look of codemagic