Chapters

Hide chapters

Real-World Flutter by Tutorials

First Edition · Flutter 3.3 · Dart 2.18 · Android Studio or VS Code

Real-World Flutter by Tutorials

Section 1: 16 chapters
Show chapters Hide chapters

14. Automated Testing
Written by Vid Palčar

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In the previous chapter, you learned how to use Firebase Crashlytics to efficiently track errors in your app. This knowledge plays an enormous role when resolving the existing issues your users face. However, the experience for your users would be even better if the app didn’t have those issues in the first place. It’s delusional to believe your app will be bug-free with your first release, but you still try to get as close to this ideal scenario as possible. You can achieve this with the help of testing.

Your QA team — or you and your fellow developers, if you work in a smaller team — can only do so much to test your app manually. That’s where automated testing can be a handy approach to make your work easier.

Besides being very limited with how much manual work you can perform — which relates directly to cost — performing automated tests also has other benefits. As people make mistakes, automated tests exclude the human error factor from app testing. With automated testing, you can get rid of repetitive work, perform the test with greater speed and consistency and test more frequently. All of these benefits result in a faster time to market.

Despite giving the impression that automated testing is this magical thing that will save you from all of the world’s problems, in some cases, manual testing is actually better. You should choose manual testing over automated testing in instances when test criteria are constantly changing, cases that aren’t routine and generally in situations when manual tests haven’t been executed yet.

You can use a few different types of automated tests for different parts of your app. To ensure that your app is well-tested, you have to provide high test coverage. This is the percentage of your app’s executed code covered by automated tests. In other words, an app with high test coverage is less likely to run into undetected bugs.

In this chapter, you’ll learn about:

  • Unit tests.
  • Widget tests.
  • Integration tests.
  • Using mocking and stubbing.
  • Writing and executing examples of each test type.

Throughout this chapter, you’ll work on the starter project from this chapter’s assets folder.

Note: If you’re having trouble running the app or the tests, you might have forgotten to propagate the configurations you did in the first chapter’s starter project to the following chapters’ materials. If that’s the case, please revisit Chapter 1, “Setting up Your Environment”.

Types of Tests

As already mentioned, there are quite a few different types of automation tests. In this chapter, you’ll mainly deal with three test types — unit tests, widget tests and integration tests. You’ll get deeper into those in just a moment.

For general knowledge purposes, it’s also worth mentioning some other types, though they’re not as important in testing mobile apps. They are:

  • Smoke testing, also known as confidence testing, is a set of tests designed to assess the stability of a deployed build.
  • Golden file testing is a special type of testing in which you compare specific behavior to the golden file. In the case of API testing, this golden file can be the response you expect from the API. On the other hand, when testing your mobile app’s UI, the golden file would be the screenshot of the UI you expect to see on your mobile device.
  • Performance testing tests the software’s speed, responsiveness and stability under the workload.

Unit Testing

Unit testing ensures that a specific unit of software behaves as intended. The term “unit” isn’t very clearly defined. It can be a complete chunk of the software but usually represents a smaller part of code, like a function or class.

Mocking and Stubbing

When performing unit tests, you must focus on the pure functionality of a specific unit. This means you should try to prevent any uncontrolled influence of other internal or external units/services with which your unit interacts. This is where you’ll use mocking and stubbing.

Widget Testing

Widget testing is a special term used specifically in testing Flutter apps. The general developer community usually refers to them as component testing.

Integration Testing

If you jump back to the section on unit testing, there was one disadvantage mentioned at the end of the section. This is where integration testing saves the day. Integration testing is a process in which you test interactions among different units and components — in this case, widgets.

Writing Tests for Flutter Apps

Open the starter project in your preferred IDE. If you quickly run through the folder structure of either the Flutter app or package, you may notice the folder called test. This is where your tests will live:

import 'package:flutter_test/flutter_test.dart';

// 1
void main() {
  // 2
  group('Group description', () {
    // 3
    setUp(() {});
    // 4
    test('Test 1 description', () {
      // 5
      expect(1, 1);
    });
    test('Test 2 description', () {
      expect(1, 1);
    });
    // 6
    tearDown(() {});
  });
  // 7
  test('Test 3 description', () {
      expect(1, 1);
    });
}

Running Flutter Tests

You can run your tests in multiple different ways. Here, you’ll explore using both your IDE and your terminal to run your tests.

flutter test test/example_test.dart
flutter test --plain-name "Group description" test/example_test.dart

Writing Unit Tests

If you think about the past chapters, you’ve learned about repositories, mappers, remote APIs, BLoC business logic, etc. In the following sections, you’ll learn how to write unit tests for all these components.

Writing Unit Tests for Mappers

Before you start writing the code, it’s worth visualizing what you want to achieve with it. In the first test, you’ll create a test for a mapper that maps DarkModePreferenceCM into DarkModePreference. So, from the extension that defines this mapper, you’d expect that in the case of DarkModePreferenceCM.alwaysDark, it would return DarkModePreference.alwaysDark. This is exactly what you’ll write in your first test.

//1
test('When mapping DarkModePreferenceCM.alwaysDark to domain, return DarkModePreference.alwaysDark',
    () {
  //2
  final preference = DarkModePreferenceCM.alwaysDark;
  //3
  expect(preference.toDomainModel(), DarkModePreference.alwaysDark);
});
import 'package:domain_models/domain_models.dart';
import 'package:key_value_storage/key_value_storage.dart';
import 'package:test/test.dart';
import 'package:user_repository/src/mappers/mappers.dart';
00:01 +1: All tests passed!

Writing a Unit Test for Your Repository

Next, you’ll write a test for your UserRepository focusing on only one function — getUserToken(). Think of the situation when the previously mentioned function will be invoked, when a successful authentication happened sometime in the past, and the token was saved in the secure storage. In this case, you’d expect from the function that it returns a valid token.

test('When calling getUserToken after successful authentication, return authentication token',
    () async {

  // TODO: add initialization of _userRepository

  expect(await _userRepository.getUserToken(), 'token');
});
// TODO: add initialization of _userSecureStorage

final _userRepository = UserRepository(
  secureStorage: _userSecureStorage,
  noSqlStorage: KeyValueStorage(),
  remoteApi: FavQsApi(
    userTokenSupplier: () => Future.value(),
  ),
);

// TODO: add stubbing for fetching token from secure storage
mockito: ^5.2.0
import 'package:fav_qs_api/fav_qs_api.dart';
import 'package:key_value_storage/key_value_storage.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:user_repository/src/user_secure_storage.dart';
import 'package:user_repository/user_repository.dart';
// TODO: add missing import

@GenerateMocks([UserSecureStorage])
flutter pub run build_runner build --delete-conflicting-outputs
final _userSecureStorage = MockUserSecureStorage();
import 'user_repository_test.mocks.dart';
00:01 +0 -1: When calling getUserToken after successful authentication, return authentication token[E]
MissingStubError: 'getUserToken'
No stub was found which matches the arguments of this method call: getUserToken()

...

00:01 +0 -1: Some tests failed.
when(_userSecureStorage.getUserToken()).thenAnswer((_) async => 'token');

Writing a Unit Test for API

In the following section, you’ll write a unit test for your signIn() function for FavQsApi. Imagine the most common scenario when the user enters the correct credentials, and the remote API returns the correct response. In this case, you expect signIn() to return an instance of the UserRM object. Again, you’ll have to stub the behavior of the remote API returning a success response. You could use the mockito package for mocking again, but this is a bit tricky when performing HTTP requests with the help of the dio package. Therefore, you’ll use the http_mock_adapter package, which makes things easier for you. First, replace # TODO add http_mock_adapter, located in packages/fav_qs_api/pubspec.yaml, with the following line, and fetch the missing packages:

http_mock_adapter: ^0.3.3
import 'package:dio/dio.dart';
import 'package:fav_qs_api/src/fav_qs_api.dart';
import 'package:fav_qs_api/src/models/models.dart';
import 'package:fav_qs_api/src/url_builder.dart';
// TODO: add missing import
import 'package:test/test.dart';

void main() {
  test(
      'When sign in call completes successfully, returns an instance of UserRM',
      () async {
    // 1
    final dio = Dio(BaseOptions());

    // TODO: add dioAdapter which will stub the expected response of remote API

    // 2
    final remoteApi =
        FavQsApi(userTokenSupplier: () => Future.value(), dio: dio);

    // 3
    const email = 'email';
    const password = 'password';

    final url = const UrlBuilder().buildSignInUrl();

    final requestJsonBody = const SignInRequestRM(
      credentials: UserCredentialsRM(
        email: email,
        password: password,
      ),
    ).toJson();

    // TODO: add an implementation of request stubbing

    // 4
    expect(await remoteApi.signIn(email, password), isA<UserRM>());
  });
}
final dioAdapter = DioAdapter(dio: dio);
import 'package:http_mock_adapter/http_mock_adapter.dart';
dioAdapter.onPost(
  url,
  (server) => server.reply(
    200,
    {"User-Token": "token", "login": "login", "email": "email"},
    delay: const Duration(seconds: 1),
  ),
  data: requestJsonBody,
);
00:02 +0: Sign in: When sign in call completes successfully, returns an instance of UserRM
*** Request ***
uri: https://favqs.com/api/session
method: POST
responseType: ResponseType.json
followRedirects: true
connectTimeout: 0
sendTimeout: 0
receiveTimeout: 0
receiveDataWhenStatusError: true
extra: {}
headers:
 Authorization: Token token=
 content-type: application/json; charset=utf-8

*** Response ***
uri: https://favqs.com/api/session
statusCode: 200
headers:
 content-type: application/json; charset=utf-8

00:02 +1: All tests passed!

Writing a BLoC Unit Test

The final unit test you’ll write in this chapter is the BLoC test. It’s very important to also test your business logic. Again, there’s a very useful library that makes testing BLoC business logic much easier: the bloc_test library. The package is already added to the pubspec.yaml folder of the sign_in package located in the packages/features folder. Now, open sign_in_cubit_test.dart, located in the packages’ test folder. In it, replace // TODO: add an implementation of BLoC test with the following code snippet:

blocTest<SignInCubit, SignInState>(
  'Emits SignInState with unvalidated email when email is changed for the first time',
  // 1
  build: () => SignInCubit(userRepository: MockUserRepository()),
  // 2
  act: (cubit) => cubit.onEmailChanged('email@gmail.com'),
  // 3
  expect: () => <SignInState>[
    const SignInState(
        email: Email.unvalidated(
      'email@gmail.com',
    ))
  ],
);
import 'package:bloc_test/bloc_test.dart';
import 'package:form_fields/form_fields.dart';
import 'package:mockito/mockito.dart';
import 'package:sign_in/src/sign_in_cubit.dart';
import 'package:user_repository/user_repository.dart';

class MockUserRepository extends Mock implements UserRepository {}

Writing a Widget Test

In the following section, you’ll write a widget test. To perform it, you’ll use the widgetTest() function, which is also implemented as a part of the flutter_test package. You’ll start with the implementation of the widget test. You’ll test if the FavoriteIconButton recognizes the onTap gesture. Open favorite_icon_button_widget_test.dart, located in the test folder of the same package component_library. In it, replace // TODO: add an implementation of widgetTest with the following code:

testWidgets('onTap() callback is executed when tapping on button',
    (tester) async {

  // 1
  bool value = false;

  // 2
  await tester.pumpWidget(MaterialApp(
    locale: const Locale('en'),
    localizationsDelegates: const [ComponentLibraryLocalizations.delegate],
    home: Scaffold(
      body: FavoriteIconButton(
        isFavorite: false,
        // 3
        onTap: () {
          value = !value;
        }),
      ),
   ));

  // 4
  await tester.tap(find.byType(FavoriteIconButton));
  // 5
  expect(value, true);
});

Writing an Integration Test

There’s only one more test type that you should write to make sure that your app potentially runs without bugs — the integration test. This test is a bit different from the ones you’ve run so far, as it requires a physical device or emulator to execute. For performing integration tests for a Flutter app, you need the integration_test package.

integration_test:
  sdk: flutter
import 'package:integration_test/integration_test.dart';
// 1
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('search for life quotes', (tester) async {
  // 2
  app.main();
  // 3
  await tester.pumpAndSettle(const Duration(seconds: 1));
  // 4
  final searchBarFinder = find.byType(SearchBar);
  // 5
  expect(searchBarFinder, findsOneWidget);
  // 6
  await tester.enterText(searchBarFinder, 'life');
  // 7
  await tester.pumpAndSettle();
  // 8
  expect(find.byType(QuoteCard), findsWidgets);
  });
flutter test integration_test/app_test.dart --dart-define=fav-qs-app-token=YOUR_TOKEN

Challenges

To test your understanding of the topic, try to write a few automated tests on your own. There are a few examples ready for you:

Key Points

  • Automation testing is a crucial part of software development, as it helps you provide a bugless app to your users.
  • A well-tested app has high test coverage.
  • Three types of tests are important when testing a Flutter app — unit tests, widget tests and integration tests.
  • Unit tests are mainly used to test smaller chunks of code, such as functions or objects.
  • Widget tests, also known as component tests, are used to test the behavior and appearance of a single widget or tree of widgets.
  • Integration tests play a significant role when testing interactions among different widgets and units.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now