Writing Unit Test In Flutter With Mockito

Writing Unit Test In Flutter With Mockito

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