Using dependency Injection to mock Component Test

Quentin Ng
5 min readNov 25, 2019

In this tutorial we’re going to learn how to use dependency injection to mock and write component test.

We will be creating a simple app that does the following:

First Page

Clicking on the Nothing Selected will show the following page

Selecting an item will show the following on the first page

First component to create is a generic select_option widget. Since it’s pretty simple component that takes a set of options and draw the option, we’ll make it a Stateless widget. The state of the widget is injected using the constructor.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:myapp/select_option.dart';
class SelectWidget extends StatelessWidget {
final List<SelectOption> options;

SelectWidget({this.options});
@override
Widget build(BuildContext context) {
if (options == null || options.length == 0) {
return Container(
key: const Key('selectWidget'), child: Text('No options available'));
}
return ListView.builder(
key: const Key('selectWidget'),
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
SelectOption option = options[index];
return Card(
child: ListTile(
title: Text(option.display),
onTap: () {
Navigator.pop(context, option);
},
));
});
}
}
// And the Select Option looks like this
class SelectOption {
String name;
String display;
SelectOption({this.name, this.display});
}

Now writing some test for a stateless widget is pretty simple. We just pass different parameters through the constructor to test the widget renders as expected.

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/select_option.dart';
import 'package:myapp/select_widget.dart';
void main() {
testWidgets(
'Given I have a list of options, then I display the option display text for each option',
(WidgetTester tester) async {
List<SelectOption> options = []
..add(SelectOption(name: 'one', display: 'One'))
..add(SelectOption(name: 'two', display: 'Two'))
..add(SelectOption(name: 'three', display: 'Three'));
await tester.pumpWidget(MaterialApp(home: SelectWidget(options: options)));
final findOne = find.text('One');
final findTwo = find.text('Two');
final findThree = find.text('Three');
expect(findOne, findsOneWidget);
expect(findTwo, findsOneWidget);
expect(findThree, findsOneWidget);

final findOneLower = find.text('one');
final findTwoLower = find.text('two');
final findThreeLower = find.text('three');
expect(findOneLower, findsNothing);
expect(findTwoLower, findsNothing);
expect(findThreeLower, findsNothing);
});
testWidgets(
'Given I have an empty list of options, then I display text "No options available" container',
(WidgetTester tester) async {
List<SelectOption> options = [];
await tester.pumpWidget(MaterialApp(home: SelectWidget(options: options)));
final findNoOptionsText = find.text('No options available');
expect(findNoOptionsText, findsOneWidget);
});
testWidgets(
'Given I have an null list of options, then I display text "No options available" container',
(WidgetTester tester) async {
List<SelectOption> options;
await tester.pumpWidget(MaterialApp(home: SelectWidget(options: options)));
final findNoOptionsText = find.text('No options available');
expect(findNoOptionsText, findsOneWidget);
});
}

Now we manage the state of the select widget by having the parent component as stateful.

import 'package:flutter/material.dart';
import 'package:myapp/async_service.dart';
import 'package:myapp/select_option.dart';
import 'package:myapp/select_widget.dart';
class SelectOptionWidget extends StatefulWidget {
final AsyncService asyncService;
SelectOptionWidget({asyncService})
: this.asyncService =
asyncService == null ? AsyncService() : asyncService;
@override
_StatefulWidgetState createState() =>
_StatefulWidgetState(asyncService: this.asyncService);
}
class _StatefulWidgetState extends State<SelectOptionWidget> {
String _selectedItem;
List<SelectOption> _selectOptions;
final AsyncService asyncService;
_StatefulWidgetState({this.asyncService});
@override
void initState() {
super.initState();
this._selectedItem = "Nothing Selected.";
this._initOptions();
}
Future<void> _initOptions() async {
List<SelectOption> option = await asyncService.getOptions();
setState(() {
this._selectOptions = option;
});
}
Future<SelectOption> selectOption() async {
return await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SelectWidget(
options: this._selectOptions,
)),
) as SelectOption;
}
@override
Widget build(BuildContext context) {
return Column(
key: const Key('selectedTextWidget'),
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
GestureDetector(
onTap: () async {
SelectOption theOption = await this.selectOption();
setState(() {
this._selectedItem = theOption.display + ' was selected';
});
},
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(15.0, 15.0, 0, 15.0),
child: Icon(
Icons.link,
),
),
Padding(
padding: const EdgeInsets.all(15.0),
child: Text(_selectedItem,
style: TextStyle(
fontWeight: FontWeight.bold,
)),
),
],
),
)
],
);
}
}
// And an implementation of some async function. Just pretend this is making a API call
class AsyncService {
Future<List<SelectOption>> getOptions() async {
List<SelectOption> options = []
..add(SelectOption(name: 'one', display: 'One'))
..add(SelectOption(name: 'two', display: 'Two'))
..add(SelectOption(name: 'three', display: 'Three'));
return options;
}
}

Notice I’m using dependency injection on the stateful widget.

class SelectOptionWidget extends StatefulWidget {
final AsyncService asyncService;
SelectOptionWidget({asyncService})
: this.asyncService =
asyncService == null ? AsyncService() : asyncService;
@override
_StatefulWidgetState createState() =>
_StatefulWidgetState(asyncService: this.asyncService);
}

This will have the asyncService passed in as an optional parameter. If no dependency is passed in, we default it. This way we can create a mock implementation for testing the component and for the real world we don’t need to pass anything in as it will default to the real implementation.

Now here are the test

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:myapp/async_service.dart';
import 'package:myapp/select_option.dart';
import 'package:myapp/select_option_widget.dart';
class MockClient extends Mock implements AsyncService {}void main() {
testWidgets('Given the initial state, I should display nothing selected',
(WidgetTester tester) async {
final client = MockClient();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Center(child: SelectOptionWidget(asyncService: client)))));
final findNothingSelectedText = find.text('Nothing Selected.');
expect(findNothingSelectedText, findsOneWidget);
});
testWidgets(
'Given the initial state, ' +
'when I open the select I should open the other widget ' +
'and when click on an item the parent is updated',
(WidgetTester tester) async {
final client = MockClient();
List<SelectOption> options = []
..add(SelectOption(name: 'one', display: 'One'))
..add(SelectOption(name: 'two', display: 'Two'))
..add(SelectOption(name: 'three', display: 'Three'));
when(client.getOptions()).thenAnswer((_) async => options);
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Center(child: SelectOptionWidget(asyncService: client)))));
final findNothingSelectedText = find.text('Nothing Selected.');
expect(findNothingSelectedText, findsOneWidget);
await tester.tap(find.text('Nothing Selected.'));
// Rebuild the widget after the state has changed.
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('One'), findsOneWidget);
await tester.tap(find.text('One'));
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('One was selected'), findsOneWidget);
});
}

Have a look at the test carefully. We’re using a Mock implementation of AsyncService. By using dependency injection we can respond to the getOptions with a Mock return value.

final client = MockClient();
when(client.getOptions()).thenAnswer((_) async => options);

Now when we test calling the second widget, the mock service returns a mock option.

    await tester.tap(find.text('Nothing Selected.'));    // Rebuild the widget after the state has changed.
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('One'), findsOneWidget);

And we can test that the new widget loaded because we can find the text ‘One’.

Now when we select ‘One’ the option is returned to the first page and updated.

    await tester.tap(find.text('One'));
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('One was selected'), findsOneWidget);

That’s it!

Couple of key take aways:

  1. I don’t like static methods, it makes it impossible to write mock test. Take the time to wrap it in a service and inject it. You can use the technique I have above to default the service if nothing is passed in.
  2. I don’t like using using Future Builder in Stateless widgets it creates side effects and again it makes it hard to write test. Using constructor to send in immutable properties makes the widget easy to understand and test.
  3. I do like thinking about Test first, it makes the code cleaner.
  4. I do like stateless widgets that are controlled using constructor parameters, you know what you’re getting.
  5. I do like using stateful widget that call stateless widgets read about Higher Order Components in React.

Want to look at the repo:

https://github.com/qng5150/flutter-test

--

--

Quentin Ng

A passion for Humanity in Computer Design to inform, engage and streamline the business process.