Using dependency Injection to mock Component Test

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});
}
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);
});
}
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;
}
}
class SelectOptionWidget extends StatefulWidget {
final AsyncService asyncService;
SelectOptionWidget({asyncService})
: this.asyncService =
asyncService == null ? AsyncService() : asyncService;
@override
_StatefulWidgetState createState() =>
_StatefulWidgetState(asyncService: this.asyncService);
}
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);
});
}
final client = MockClient();
when(client.getOptions()).thenAnswer((_) async => options);
    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);
  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.

--

--

--

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

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Four Fake Nice Gestures That Are Actually Manipulative 2021

How I Discovered My Desire to Learn to Code

brian-2x

My poor neglected Blog!

I hereby start a series about Web3 Projects — Whats out there? What’s good, what's bad? We’ll see…

Best operating system, Android or iOS

Akumaverse Gathering #1

Top 3 Microservices Book In 2021 For Software Developers and Architects

Database Management Systems for Big Data Requirements

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Quentin Ng

Quentin Ng

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

More from Medium

Create android emulator with command line

How to setup Flutter development environment for Android on Linux… I mean Windows

11 Ways to Boost Your Android Studio Flutter Development Productivity

USB Debugging using Android Debug Bridge