Created
August 16, 2019 13:37
-
-
Save brianegan/414f6b369c534a0e5f20bff377823414 to your computer and use it in GitHub Desktop.
Demonstrates how to Mock Futures in Widiget tests to check the various expected ouputs depending on the loading / success / error state of the Future
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is a basic Flutter widget test. | |
// | |
// To perform an interaction with a widget in your test, use the WidgetTester | |
// utility that Flutter provides. For example, you can send tap and scroll | |
// gestures. You can also use WidgetTester to find child widgets in the widget | |
// tree, read text, and verify that the values of widget properties are correct. | |
import 'dart:convert'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter_test/flutter_test.dart'; | |
import 'package:http/http.dart' as http; | |
import 'package:mockito/mockito.dart'; | |
import 'package:provider/provider.dart'; | |
import 'package:future_testing/main.dart'; | |
class MockApiClient extends Mock implements ApiClient {} | |
void main() { | |
group('PostScreen', () { | |
testWidgets('starts with a loading spinner', (tester) async { | |
final client = MockApiClient(); | |
final widget = TestWidget(client: client); | |
// Mock out the Future<Post> with an async function that returns a | |
// Dummy Post for testing | |
when(client.fetchPost()).thenAnswer((_) async => Post(title: 'A')); | |
// Mock out a Future<void> by answering with an async function that | |
// does not return anything | |
when(client.savePost()).thenAnswer((_) async {}); | |
await tester.pumpWidget(widget); | |
expect(find.byType(CircularProgressIndicator), findsOneWidget); | |
}); | |
testWidgets('loads and shows a post', (tester) async { | |
final client = MockApiClient(); | |
final widget = TestWidget(client: client); | |
when(client.fetchPost()).thenAnswer((_) async => Post(title: 'P')); | |
when(client.savePost()).thenAnswer((_) async {}); | |
// First pump builds the Widget with the future uncompleted | |
await tester.pumpWidget(widget); | |
// Second pump builds the Widget after the future returns | |
await tester.pumpWidget(widget); | |
expect(find.text('P'), findsOneWidget); | |
}); | |
testWidgets('loads and shows an error', (tester) async { | |
final client = MockApiClient(); | |
final widget = TestWidget(client: client); | |
// Use an async function that throws to simulate a future error | |
when(client.fetchPost()).thenAnswer((_) async => throw 'E'); | |
when(client.savePost()).thenAnswer((_) async {}); | |
// First pump builds the Widget with the future uncompleted | |
await tester.pumpWidget(widget); | |
// Second pump builds the Widget after the future throws an error | |
await tester.pumpWidget(widget); | |
expect(find.text('E'), findsOneWidget); | |
}); | |
testWidgets('calls savePost when the button is tappepd', (tester) async { | |
final client = MockApiClient(); | |
final widget = TestWidget(client: client); | |
// Use an async function that throws to simulate a future error | |
when(client.fetchPost()).thenAnswer((_) async => Post(title: 'A')); | |
when(client.savePost()).thenAnswer((_) async {}); | |
// First pump builds the Widget with the future uncompleted | |
await tester.pumpWidget(widget); | |
await tester.tap(find.byKey(Key('save_button'))); | |
// Use mockito to verify savePost has been called twice: Once when the | |
// Widget is first shown and a second time when the save button is pressed | |
verify(client.savePost()).called(2); | |
}); | |
}); | |
} | |
// A Widget to setup up the Provider and Directionality widgets for the tests in | |
// this file | |
class TestWidget extends StatelessWidget { | |
final ApiClient client; | |
const TestWidget({Key key, this.client}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Directionality( | |
textDirection: TextDirection.ltr, | |
child: Provider<ApiClient>( | |
builder: (BuildContext context) => client, | |
child: PostScreen(), | |
), | |
); | |
} | |
} | |
class PostScreen extends StatefulWidget { | |
@override | |
_PostScreenState createState() => _PostScreenState(); | |
} | |
class _PostScreenState extends State<PostScreen> { | |
Future<Post> _postFuture; | |
@override | |
void initState() { | |
// Get the ApiClient from the Provider. | |
// | |
// In your real app, you'd Provide the normal ApiClient. In tests, | |
// provide a MockApiClient. | |
// | |
// If you aren't using Provider, you can also supply the ApiClient directly | |
// to the Widget for testing and use `widget.client.fetchPost`. | |
final client = Provider.of<ApiClient>(context, listen: false); | |
// Fetch the post | |
_postFuture = client.fetchPost(); | |
// Do some work that doesn't return anything useful (Future<void>). | |
client.savePost(); | |
super.initState(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Column( | |
children: <Widget>[ | |
FutureBuilder<Post>( | |
future: _postFuture, | |
builder: (context, snapshot) { | |
if (snapshot.hasData) { | |
return Text(snapshot.data.title); | |
} else if (snapshot.hasError) { | |
return Text('${snapshot.error}'); | |
} else { | |
return CircularProgressIndicator(); | |
} | |
}, | |
), | |
FlatButton( | |
key: Key('save_button'), | |
onPressed: () { | |
Provider.of<ApiClient>(context, listen: false).savePost(); | |
}, | |
child: Text('Save Post'), | |
) | |
], | |
); | |
} | |
} | |
class ApiClient { | |
Future<void> savePost() async {} | |
Future<Post> fetchPost() async { | |
final response = | |
await http.get('https://jsonplaceholder.typicode.com/posts/1'); | |
if (response.statusCode == 200) { | |
// If the call to the server was successful, parse the JSON. | |
return Post.fromJson(json.decode(response.body)); | |
} else { | |
// If that call was not successful, throw an error. | |
throw Exception('Failed to load post'); | |
} | |
} | |
} | |
class Post { | |
final int userId; | |
final int id; | |
final String title; | |
final String body; | |
Post({this.userId, this.id, this.title, this.body}); | |
factory Post.fromJson(Map<String, dynamic> json) { | |
return Post( | |
userId: json['userId'], | |
id: json['id'], | |
title: json['title'], | |
body: json['body'], | |
); | |
} | |
} |
Thanks @brianegan for this helpful gist!
For those also fairly new to using provider
s and deciding to take that route to access the injected API client instance for both running the app as well as testing, using provider
v2.0.xx
for this sample did it for me FYI
Hey @brianegan I came upwith an idea to use eBay's Golden Toolkit to write all the widget tests in form of Golden Test Driven Development, this ideaof a TestWidget to supply state and dapinjection I am borrowing as it's a good idea and implementation.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Oh never mind, I got what was going on.