The following tools/software must be up and running :
In order to work with code coverage, we will need lcov
For MAC
$ brew install lcov
For UBUNTU
$ sudo apt-get update -qq -y
$ sudo apt-get install lcov -y
If you are able to use docker
$ docker pull bwnyasse/flutter-cirrusci-coverage
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.
If you have a github account, you can fork this project
Checkout the working repository
# Clone the original project or your github fork copy
$ git clone https://github.com/bwnyasse/flutter_testing_tutorial.git
Import the project in your editor
Launch an emulator or connect a device to ensure that the application is running
$ flutter run
Understand the goal of the project by reading the README.md
_ _ _ _ _ _ _ ___| |_ __ _ _ __| |_ | |_| |__ ___ ___ ___ __| | ___| | __ _| |__ / __| __/ _` | '__| __| | __| '_ \ / _ \ / __/ _ \ / _` |/ _ \ |/ _` | '_ \ \__ \ || (_| | | | |_ | |_| | | | __/ | (_| (_) | (_| | __/ | (_| | |_) | |___/\__\__,_|_| \__| \__|_| |_|\___| \___\___/ \__,_|\___|_|\__,_|_.__/
git checkout -f step1-TODO
In order to mock a service making a http request, we must provide a sample response from the server.
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.
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';
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!
_ _ _ _ _ _ _ ___| |_ __ _ _ __| |_ | |_| |__ ___ ___ ___ __| | ___| | __ _| |__ / __| __/ _` | '__| __| | __| '_ \ / _ \ / __/ _ \ / _` |/ _ \ |/ _` | '_ \ \__ \ || (_| | | | |_ | |_| | | | __/ | (_| (_) | (_| | __/ | (_| | |_) | |___/\__\__,_|_| \__| \__|_| |_|\___| \___\___/ \__,_|\___|_|\__,_|_.__/
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:
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:
_ _ _ _ _ _ _ ___| |_ __ _ _ __| |_ | |_| |__ ___ ___ ___ __| | ___| | __ _| |__ / __| __/ _` | '__| __| | __| '_ \ / _ \ / __/ _ \ / _` |/ _ \ |/ _` | '_ \ \__ \ || (_| | | | |_ | |_| | | | __/ | (_| (_) | (_| | __/ | (_| | |_) | |___/\__\__,_|_| \__| \__|_| |_|\___| \___\___/ \__,_|\___|_|\__,_|_.__/
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:
Add the following code in the test('posterPath is null'
Movie m = Movie();
expect(m.posterPathResolved, equals('https://via.placeholder.com/300'));
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:
_ _ _ _ _ _ _ ___| |_ __ _ _ __| |_ | |_| |__ ___ ___ ___ __| | ___| | __ _| |__ / __| __/ _` | '__| __| | __| '_ \ / _ \ / __/ _ \ / _` |/ _ \ |/ _` | '_ \ \__ \ || (_| | | | |_ | |_| | | | __/ | (_| (_) | (_| | __/ | (_| | |_) | |___/\__\__,_|_| \__| \__|_| |_|\___| \___\___/ \__,_|\___|_|\__,_|_.__/
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:
In order to initialize the bloc AppBloc
, we need to mock the application service AppService
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]),
);
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),
);
});
_ _ _ _ _ _ _ ___| |_ __ _ _ __| |_ | |_| |__ ___ ___ ___ __| | ___| | __ _| |__ / __| __/ _` | '__| __| | __| '_ \ / _ \ / __/ _ \ / _` |/ _ \ |/ _` | '_ \ \__ \ || (_| | | | |_ | |_| | | | __/ | (_| (_) | (_| | __/ | (_| | |_) | |___/\__\__,_|_| \__| \__|_| |_|\___| \___\___/ \__,_|\___|_|\__,_|_.__/
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:
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
_ _ _ _ _ _ _ ___| |_ __ _ _ __| |_ | |_| |__ ___ ___ ___ __| | ___| | __ _| |__ / __| __/ _` | '__| __| | __| '_ \ / _ \ / __/ _ \ / _` |/ _ \ |/ _` | '_ \ \__ \ || (_| | | | |_ | |_| | | | __/ | (_| (_) | (_| | __/ | (_| | |_) | |___/\__\__,_|_| \__| \__|_| |_|\___| \___\___/ \__,_|\___|_|\__,_|_.__/
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.
Fix the TODO 1- Initialize the key with the movie.id
Fix the TODO 2- Find objects to matchWith the following code:
final movieFinder = find.byType(MovieCard);
expect(movieFinder, findsOneWidget);
Finder textFinder = find.text(movie.title);
expect(textFinder, findsOneWidget);
textFinder = find.text(movie.overview);
expect(textFinder, findsOneWidget);
textFinder = find.text(movie.releaseDate);
expect(textFinder, findsOneWidget);
Run the following command
flutter test
_ _ _ _ _ _ _ ___| |_ __ _ _ __| |_ | |_| |__ ___ ___ ___ __| | ___| | __ _| |__ / __| __/ _` | '__| __| | __| '_ \ / _ \ / __/ _ \ / _` |/ _ \ |/ _` | '_ \ \__ \ || (_| | | | |_ | |_| | | | __/ | (_| (_) | (_| | __/ | (_| | |_) | |___/\__\__,_|_| \__| \__|_| |_|\___| \___\___/ \__,_|\___|_|\__,_|_.__/
git checkout -f step7-TODO
1- Run the following command
flutter test
_ _ _ _ _ _ _ ___| |_ __ _ _ __| |_ | |_| |__ ___ ___ ___ __| | ___| | __ _| |__ / __| __/ _` | '__| __| | __| '_ \ / _ \ / __/ _ \ / _` |/ _ \ |/ _` | '_ \ \__ \ || (_| | | | |_ | |_| | | | __/ | (_| (_) | (_| | __/ | (_| | |_) | |___/\__\__,_|_| \__| \__|_| |_|\___| \___\___/ \__,_|\___|_|\__,_|_.__/
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
_ _ _ _ _ _ _ ___| |_ __ _ _ __| |_ | |_| |__ ___ ___ ___ __| | ___| | __ _| |__ / __| __/ _` | '__| __| | __| '_ \ / _ \ / __/ _ \ / _` |/ _ \ |/ _` | '_ \ \__ \ || (_| | | | |_ | |_| | | | __/ | (_| (_) | (_| | __/ | (_| | |_) | |___/\__\__,_|_| \__| \__|_| |_|\___| \___\___/ \__,_|\___|_|\__,_|_.__/
git checkout -f step9-TODO
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.
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.
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:
--target
app and installs it on the emulator / device.app_test.dart
test suite located in test_driver/
folder._ _ _ _ _ _ _ ___| |_ __ _ _ __| |_ | |_| |__ ___ ___ ___ __| | ___| | __ _| |__ / __| __/ _` | '__| __| | __| '_ \ / _ \ / __/ _ \ / _` |/ _ \ |/ _` | '_ \ \__ \ || (_| | | | |_ | |_| | | | __/ | (_| (_) | (_| | __/ | (_| | |_) | |___/\__\__,_|_| \__| \__|_| |_|\___| \___\___/ \__,_|\___|_|\__,_|_.__/
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
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.yaml
to setup your code coverage 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.
Let's have a look of codemagic