Flutter: How To Mock Functions For Testing

November 17, 2022
Flutter

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.

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.

Type Alias



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

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.


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.



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.


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

My photoChristian Findlay

I am a Flutter, .NET and Cloud Developer living in Melbourne, Australia. I've been developing software and mentoring teams for over twenty years.

Related Posts