In this article, we'll be writing unit test for a simple flutter application that makes use of the random joke api.
Unit test is all about writing code to verify/validate the behavior of a function, method or class. This way, with more features being added or changes in certain functionalities one can ensure that the app continues to work as intended.
In order to write code that is easily testable, external classes shouldn't be created and instantiated within our own class instead it should be passed in through the constructor. For example instead of this
import 'package:http/http.dart' as http;
class MyClass {
Future<String> getUserName() async{
final response = await http.get('www.example.com/username');
return response.body;
}
}
We would do this
import 'package:http/http.dart' as http;
class MyClass {
final http.Client client;
MyClass({this.client});
Future<String> getUserName() async{
final response = await client.get('www.example.com/username');
return response.body;
}
}
This is because when test is written for the getUserName() function above there wouldn't actually be a real client.get() call. Because a mocked http client (using mockito package) whose behavior can be controlled would be passed in through the constructor instead.
In other words, function calls from external packages (e.g. client.get(), client.post()) would be 'simulated' while we focus on how our own logic should work.
Building the App
Firstly we are going to create a simple app before we write the test. Start by creating a flutter project called example then navigate into the folder
flutter create example
cd example
Ensure you have the required packages in your pupspec.yaml file, for this example that would be "http" and "mockito".
dependencies:
flutter:
sdk: flutter
http: ^0.13.1
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^4.1.3
Then run flutter pub get to get the packages
flutter pub get
Create a new file called api.dart under your lib folder and paste in the following code
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';
class Api {
final http.Client client;
Api({@required this.client});
Future<String> getRandomJoke() async {
try {
final response = await client.get(
Uri.parse('https://official-joke-api.appspot.com/random_joke'),
headers: {
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
final joke = json.decode(response.body);
return '${joke['setup']}\n${joke['punchline']}';
} else {
return 'Something went wrong';
}
} catch (error) {
return 'An error occurred';
}
}
}
This is what a typical response from the api call above looks like
{
"id": last joke id + 1,
"type": "programming",
"setup": "What's the best thing about a Boolean?",
"punchline": "Even if you're wrong, you're only off by a bit."
}
Replace the code in your main.dart with the following
import 'package:example/api.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _joke = '';
bool _isLoading = false;
Api _api = Api(client: http.Client());
void _getJoke() async {
setState(() {
_isLoading = true;
});
_joke = await _api.getRandomJoke();
setState(() {
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Unit test example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_joke, textAlign: TextAlign.center),
_isLoading
? CircularProgressIndicator()
: ElevatedButton(
onPressed: _getJoke,
child: Text('Get Joke'),
)
],
),
),
);
}
}
Now run the app using
flutter run
And you should see a very simple app with a button that loads a random joke anytime it is pressed.
Writing the test
Now we can move on to actually writing test for our Api class. To do this create a new file called api_test.dart under the test folder. Test files should always end with _test.dart, this is the convention used by the test runner when searching for tests.
We then create a mocked http Client class that would be passed to our Api class. This is done by simply creating a dart class that extends Mock (from mockito package) and implements the class we wish to mock (in this case Client from http)
class MockHttpClient extends Mock implements Client {}
Within the main function we would create instances of the classes we need (MockHttpClient and Api) and initialize them within setUp (from flutter_test package) which runs before every test.
void main() {
MockHttpClient mockHttpClient;
Api api;
setUp(() {
mockHttpClient = MockHttpClient();
api = Api(client: mockHttpClient);
});
}
Now with the mocked http Client passed to our Api class we can control how it behaves within our function, like returning a response of our choice when a certain method is called. For example
when(mockHttpClient.get(any, headers: anyNamed('headers'))).thenAnswer(
(_) => Future.value(Response('body', 200)),
);
"any" can be used in place of positional arguments.
"anyNamed(parameterName)" can be used in place of named arguments
".thenAnswer()" is used when the function called within when() resolves to a Future or Stream
".thenReturn()" is used when the function called within when() isn't of type Future or Stream
".thenThrow()" does what it says, it throws exceptions, or regular Error objects
Tests are defined using the test function, and you can check if the expected results are correct by using the expect function. Both of these functions come from the test package. When writing unit test you should at least test the success and error case of the function. For our getRandomJoke method we would be testing three different scenarios.
- when status code from the response is 200. For this we return a Response object with a status code of 200 and an actual body (json String) similar to what we expect to get
test('Should return a random joke when successful', () async {
when(
mockHttpClient.get(any, headers: anyNamed('headers')),
).thenAnswer(
(_) => Future.value(Response(
'{"setup": "the setup", "punchline": "the punchline"}', 200)),
);
final joke = await api.getRandomJoke();
expect(joke, equals('the setup\nthe punchline'));
});
- when status code is not 200
test('Should return "Something went wrong" when un-successful', () async {
when(
mockHttpClient.get(any, headers: anyNamed('headers')),
).thenAnswer(
(_) => Future.value(Response('Response body', 400)),
);
final joke = await api.getRandomJoke();
expect(joke, equals('Something went wrong'));
});
- when an error occurs
test('Should return "An error occurred" when error occurs', () async {
when(
mockHttpClient.get(any, headers: anyNamed('headers')),
).thenThrow(Error());
final joke = await api.getRandomJoke();
expect(joke, equals('An error occurred'));
});
Running the test
To run all the test, you can either click the "Run" above the main method in your api_test.dart file or from the command line simply make sure you're within the app's directory (e.g.
C:\Users\ifeanyi\FlutterProjects\example>
) and run flutter test.
flutter test
00:01 +3: All tests passed!
If you have several tests that are related to one another, combine them using the group function which is also provided by the test package.
Complete api_test.dart file
import 'package:example/api.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart';
import 'package:mockito/mockito.dart';
class MockHttpClient extends Mock implements Client {}
void main() {
MockHttpClient mockHttpClient;
Api api;
setUp(() {
mockHttpClient = MockHttpClient();
api = Api(client: mockHttpClient);
});
group('Api class GetRandomJoke', () {
test('Should return a random joke when successful', () async {
when(
mockHttpClient.get(any, headers: anyNamed('headers')),
).thenAnswer(
(_) => Future.value(Response(
'{"setup": "the setup", "punchline": "the punchline"}', 200)),
);
final joke = await api.getRandomJoke();
expect(joke, equals('the setup\nthe punchline'));
});
test('Should return "Something went wrong" when un-successful', () async {
when(
mockHttpClient.get(any, headers: anyNamed('headers')),
).thenAnswer(
(_) => Future.value(Response('Error 400', 400)),
);
final joke = await api.getRandomJoke();
expect(joke, equals('Something went wrong'));
});
test('Should return "An error occured" when error occurs', () async {
when(
mockHttpClient.get(any, headers: anyNamed('headers')),
).thenThrow(Error());
final joke = await api.getRandomJoke();
expect(joke, equals('An error occured'));
});
});
}
Thanks for reading
For more information on how to use mockito, see the documentation provided by the Mockito package.
The flutter_test_library is also a good reference
Check out the full code here