Skip to content

Instantly share code, notes, and snippets.

@brianegan
Created August 16, 2019 13:37
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save brianegan/414f6b369c534a0e5f20bff377823414 to your computer and use it in GitHub Desktop.
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 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'],
);
}
}
@OsamaAldawoody
Copy link

image
Is this true?

@fredgrott
Copy link

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