Flutter: How To Mock Functions For Testing

17 Dec 2022 By Christian Findlay
Build App Icon

Christian is the Director of Nimblesite

Nimblesite specializes in building and maintaining Flutter apps. Call Nimblesite in Australia on 1300 794 205 to hire an expert.

HIRE A FLUTTER EXPERT

The Dart language allows us to implement and mock any class. That’s because Dart has implicit interfaces, which is great for testing. However, some libraries give us functions that don’t belong to a class. That means we must do some work to mock or fake these functions for tests. This article explains how to do that.

What is the Issue With Functions?

If we call a standard top-level function in Dart, we cannot mock that function for a test. A classic case is the launchUrl function in the url_launcher library. When we run a widget test, we don’t want the launcher to open the Url physically, but we may want to put a fake in place, so we know that the app did call the function. We need an abstraction for the function.

A typical solution is to create a new class and add a method with the same signature as the launchUrl function. This does work, but it’s overkill. Functions are simpler than classes and require less maintenance. We shouldn’t add classes to our system only to call functions. This is especially true if we aim toward a more functional programming style. This Dart lint rule says it all. I recommend turning this rule on if you can.

Unlike Java, Dart has first-class functions, closures, and a nice light syntax for using them. If all you need is something like a callback, just use a function. If you’re defining a class and it only has a single abstract member with a meaningless name like call or invoke, there is a good chance you just want a function.

How Do We Add An Abstraction?

Functions are first-class objects in Dart. We can pass references to them around just as though we pass other objects around.

Every function in Dart has a signature based on the parameters and their types. We can use this signature for type safety and declare type aliases or inline function types. The type alias or function type is the abstraction.

This code declares a type alias SomeFunction, a function with that signature assigns the function to a variable of type SomeFunction and calls the variable to return a value to result.

typedef SomeFunction = String Function();
String someFunction() => 'Some function';
SomeFunction somefunction = someFunction;

Abstraction

Pass the Function Into a Widget

Let’s start by creating a type alias for the launchUrl function. The first thing we notice about the launchUrl function is that it has default values and the parameters are not nullable. Function types cannot have default values, so we can only approximate the launchUrl type alias. This is a good approximation. You could remove the optional parameters if you want to simplify this.

//A type alias for the launchUrl's function signature
typedef LaunchUrl = Future<bool> Function(
Uri url, {
//We accept nulls here because we cannot use default
//values in type aliases
LaunchMode? mode,
WebViewConfiguration? webViewConfiguration,
String? webOnlyWindowName,
}
);

Type Alias

We can now pass the function into our Widget, BloC, ChangeNotifier, or other classes.

class MyApp extends StatelessWidget {
const MyApp ( {
required this.launchUrl, super.key,
);
final LaunchUrl launchUri:
Widget
@override
Widget build(BuildContext context){
...

Widget

Wiring It Up

We want to be able to replace the function with a mock or fake at testing time, so we need to use some dependency injection or service location that will allow us to substitute the function later. In this example, we will use the ioc_container package, but you can use Provider or a service locator. We add the package with

flutter pub add ioc_container

This code adds the launchUrl function as a dependency to our container in the compose function and injects the launchUrl function into the MyApp widget and we only need one line to run the app. You will also need the url_launcher package for this example.

import 'package:flutter/material.dart';
import 'package:ioc_container/ioc_container.dart';
import 'package:url_launcher/url_launcher.dart';

final flutterDevUri = Uri.parse('https://www.flutter.dev');

///A type alias for the launchUrl's function signature
typedef LaunchUrl = Future<bool> Function(
  Uri url, {
  //We accept nulls here because we cannot use default
  //values in type aliases
  LaunchMode? mode,
  WebViewConfiguration? webViewConfiguration,
  String? webOnlyWindowName,
});

///Composition root. This is where we wire up dependencies
IocContainerBuilder compose() => IocContainerBuilder(
      allowOverrides: true,
    )
      ..addSingletonService<LaunchUrl>(
        //This is how we map the LaunchUrl type on to the actual function
        //Yes, this is a bit clunky, but using it in the app is very simple
        (url, {mode, webOnlyWindowName, webViewConfiguration}) async =>
           launchUrl(
              url,
              mode: mode ?? LaunchMode.platformDefault,
              webViewConfiguration:
                  webViewConfiguration ?? const WebViewConfiguration(),
              webOnlyWindowName: webOnlyWindowName,
            ),
          )
           ..add((container) => MyApp(launchUrl: container<LaunchUrl>()));

void main() => runApp(compose().toContainer()<MyApp>());

class MyApp extends StatelessWidget {
  const MyApp({
    required this.launchUrl,
    super.key,
  });

  final LaunchUrl launchUrl;

  @override
  Widget build(BuildContext context) => MaterialApp(
        debugShowCheckedModeBanner: false,
        title: 'Function Mocking Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: Scaffold(
          body: Align(
            child: MaterialButton(
              onPressed: () async {
                await launchUrl(flutterDevUri);
              },
              color: Colors.blue,
              textColor: Colors.white,
              padding: const EdgeInsets.all(40),
              shape: const CircleBorder(),
              child: const Icon(
                Icons.favorite,
                size: 50,
              ),
            ),
          ),
        ),
      );
}

Substitute with Fake Function for Testing

The compose function returns an IocContainerBuilder. We can replace dependencies in the builder before we call toContainer(). In this widget test, we replace launchUrl with a fake function specifically to track the number of times the app calls launchUrl. The mock will add to the launches list every time the app launches a Url. After the app runs, we can verify the launch count.

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:launch/main.dart';

class UrlLaunch {
  UrlLaunch(this.uri);

  final Uri uri;
}

void main() {
  testWidgets('Test Url Launch', (tester) async {
    final launches = <UrlLaunch>[];

    final builder = compose()
      //Replace the launch function with a mock/fake
      ..addSingletonService<LaunchUrl>((
        uri, {
        mode,
        webOnlyWindowName,
        webViewConfiguration,
      }) async {
        //This happens when try to launch a Uri
        //We track how many times the launch function is called
        launches.add(UrlLaunch(uri));
        return Future.value(true);
      });

    await tester.pumpWidget(
      builder.toContainer()<MyApp>(),
    );

    //Tap the icon
    await tester.tap(
      find.byIcon(Icons.favorite),
    );

    await tester.pumpAndSettle();

    expect(launches.length, 1);
    expect(
      launches[0].uri,
      flutterDevUri,
    );
  });
}

Substitute with Mock Function for Testing

We can also create a mock with a library like Mockito or Mocktail, as I have done in this example. We need to create a class that extends Mock (part of Mocktail) and has one method called call. We can then use when to set up the return value. Lastly, we call verify to ensure that the app called launchUrl the correct number of times.

import 'package:fafsdfsdf/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/material.dart';

class LaunchMock extends Mock {
  Future<bool> call(
    Uri url, {
    LaunchMode? mode,
    WebViewConfiguration? webViewConfiguration,
    String? webOnlyWindowName,
  });
}

void main() {
  testWidgets('Test Url Launch', (tester) async {
    //These allow default values
    registerFallbackValue(LaunchMode.platformDefault);
    registerFallbackValue(const WebViewConfiguration());

    //Create the mock
    final mock = LaunchMock();
    when(() => mock(
          flutterDevUri,
          mode: any(named: 'mode'),
          webViewConfiguration: any(named: 'webViewConfiguration'),
          webOnlyWindowName: any(named: 'webOnlyWindowName'),
        )).thenAnswer((_) async => true);

    final builder = compose()
      //Replace the launch function with a mock
      ..addSingletonService<LaunchUrl>(mock);

    await tester.pumpWidget(
      builder.toContainer()<MyApp>(),
    );

    //Tap the icon
    await tester.tap(
      find.byIcon(Icons.favorite),
    );

    await tester.pumpAndSettle();

    verify(() => mock(flutterDevUri)).called(1);
  });
}

Wrap-up

Functions are dependencies, just like other objects. You don’t need to declare a class to make an abstraction. You can make abstractions for functions, and this is usually simpler. It keeps you close to the functions in the libraries you consume without obscuring them. After all, functional programming is about functions, so embrace them where you can. The ioc_container library can help you manage all types of dependencies, including top-level functions.

Photo by Joshua Miranda from Pexels