Flutter Testing

2 minute read

Unit Testing

Tests go into a void main() function.

For a single test in a single file:

import 'package:flutter_test/flutter_test.dart';

void main() {
  test('addition', (){
    expect(1 + 2, 3);
  });
}

Test groups

Tests can be grouped:

import 'package:flutter_test/flutter_test.dart';

void main() {
  group('arithmetic', (){
    test('addition', (){
      expect(1 + 2, 3);
    });
    test('subtraction', (){
      // ...
    });
    // ...etc
  });
}

Tests within a group can have setUp and tearDown methods:

void main() {
  group('arithmetic', (){
    setUp(() {
      // test set up.
    });
    test('addition', (){
      expect(1 + 2, 3);
    });
    // ...etc
    tearDown((){
      // test clean up.
    });
  });
}

Asynchronous Testing

When testing an async function which affects a stream, we can use emitsInOrder. For example:

   Stream<int> genFibbo() {
    return Stream<int>.fromIterable([0, 1, 1, 2, 3, 5]);
  }

  test('fibbonacci', () {
    expect(
      genFibbo(),
      emitsInOrder([0, 1, 1, 2, 3, 5])
    );
  });
}

Widget Tests

Import your widget and use testWidgets instead of test. This gives us a WidgetTester which we can use to load widgets.

We use tester.pumpWidget(widget) to load the Widget under test.

Next we can use matchers to find properties about our widget.

import 'package:flutter_test/flutter_test.dart';
import 'package:hoani_land/common_widgets/hoani_form.dart';

void main() {
  testWidgets('empty form', (WidgetTester tester) async {
    await tester.pumpWidget(
      const HoaniForm(),
    );

    final submitButton = find.byType(SubmitButton);
    expect(submitButton, findsOneWidget);
  });
}

The WidgetTester provides a number of methods which can be used to take actions in your widget. For example, onTap:

import 'package:flutter_test/flutter_test.dart';
import 'package:hoani_land/common_widgets/hoani_form.dart';

void main() {
  testWidgets('onSubmit', (WidgetTester tester) async {
    var submitted = false;
    await tester.pumpWidget(
      const HoaniForm(
        onSubmit: () => submitted = true,
      ),
    );

    final submitButton = find.byType(SubmitButton);
    expect(submitButton, findsOneWidget);
    await tester.tap(submitButton);
    expect(submitted, true);
  });
}

Mocking with Mockito

Mockito (pub.dev) is a mocking library for dart.

Mocks need to be generated in Flutter. To do this add build_runner to dart:

dart pub add build_runner --dev

If we want to mock a Database class, in your test file add the following:

import 'package:hoani_land/services/database.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

@GenerateMocks([Database])
main() {
  //...
}

Run the mock generation:

flutter pub run build_runner build

Now we can add the following import to our test file (assuming the test file is called foo_test.dart):

import 'foo_test.mocks.dart'

You can create and use a mock in your test:

    test('no update on creation', (){
      MockDatabase database = MockDatabase();
      final tracker = ItemsTracker(
        store: 'test store', 
        database: database,
      );
      verifyNever(database.getItems(any));
    });

Any calls that are made to your mock need to be stubbed. This is done using when:

    test('calls get items on update', (){
      MockDatabase database = MockDatabase();
      when(database.getItems(any))
        .thenAnswer((_) => Stream.value('A'));
      final tracker = ItemsTracker(
        store: 'test store', 
        database: database,
      );
      verify(database.getItems(any).called(1));
    });

Note - in the above example, we used thenAnswer in our stub. The following are guidelines are useful:

  • thenReturn for functions which return immediately.
  • thenAnswer for Future or Stream.
  • thenThrow if you want to throw.