Take your Flutter tests to the next level with abstract classes and dependency injection

Andrea Bizzotto
Code With Andrea
Published in
6 min readApr 8, 2018

Today I’ll show you how to write testable code in Flutter, and take your widget tests to the next level.

Coming from the world of iOS development, I use dependency injection and Swift protocols to write testable code.

Why? So that my tests run faster, in isolation, and without side effects (no access to the network or filesystem).

After reading about unit, widget and integration tests in Flutter, I could not find guidelines about:

  • How to create protocols in Dart?
  • How to do dependency injection in Dart?

Turns out, protocols are roughly the same as abstract classes.

What about dependency injection? The Flutter docs are not helpful:

Does Flutter come with a dependency injection framework or solution?

Not at this time. Please share your ideas at flutter-dev@googlegroups.com.

So, what to do? 🤔

Short Story

  • Inject dependencies as abstract classes into your widgets.
  • Instrument your tests with mocks and ensure they return immediately.
  • Write your expectations against the widgets or your mocks.

Long Story

We’ll get into some juicy details. But first, we need a sample app.

Use case: Login form with Firebase authentication

Suppose you want to build a simple login form, like this one:

This works as follows:

  • The user can enter her email and password.
  • When the Login button is tapped, the form is validated.
  • If the email or password are empty, we highlight them in red.
  • If both email and password are non-empty, we use them to sign in with Firebase and show a confirmation message.

Here is a sample implementation for this flow:

Let’s break this down:

  • In the build() method, we create a Form to hold two TextFormFields (for email and password) and a RaisedButton (our login button).
  • The email and password fields have a simple validator that returns false if the text input is empty.
  • When the Login button is tapped, the validateAndSubmit() method is called.
  • This calls validateAndSave(), which validates the fields inside the form, and saves the _email and _password if they are non-empty.
  • If validateAndSave() returns true, we call Firebase.instance.signInWithEmailAndPassword() to sign in the user.
  • Once this call returns, we set the _authHint string. This is wrapped in a setState()method to schedule a rebuild of the LoginPage widget and update the UI.
  • The buildHintText() method uses the _authHint string to inform the user of the authentication result.

Here is a preview of our Flutter app:

So, what do we want to test here?

Acceptance criteria

We want to test the following scenarios:

Given the email or password is empty
When the user taps on the login button
Then we don’t attempt to sign in with Firebase
And the confirmation message is empty

Given the email and password are both non-empty
And they do match an account on Firebase
When the user taps on the login button
Then we attempt to sign in with Firebase
And we show a success confirmation message

Given the email and password are both non-empty
And they do not match an account on Firebase
When the user taps on the login button
Then we attempt to sign in with Firebase
And we show a failure confirmation message

Writing the first test

Let’s write the a widget test for the first scenario:

Note: When running widget tests, the build() method is not called automatically if setState()is executed. We need to explicitly call tester.pump() to trigger a new call to build().

If we type flutter test on the terminal to run our test, we get the following:

══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown building Scaffold(dirty, state: ScaffoldState#56c5e):
No MediaQuery widget found.
Scaffold widgets require a MediaQuery widget ancestor.
The specific widget that could not find a MediaQuery ancestor was:
Scaffold
The ownership chain for the affected widget is:
Scaffold ← LoginPage ← [root]
Typically, the MediaQuery widget is introduced by the MaterialApp or WidgetsApp widget at the top of your application widget tree.

As explained in this StackOverflow answer, we need to wrap our widget with a MediaQuery and a MaterialApp:

If we run this again, the test passes! ✅

Sign in tests

Let’s write a test for our second scenario:

Given the email and password are both non-empty
And they do match an account on Firebase
When the user taps on the login button
Then we attempt to sign in with Firebase
And we show a success confirmation message

If we run this test, our expectation on hintTest fails. ❌

Some debugging with breakpoints reveals that this test returns before we reach the setState() line after signInWithEmailAndPassword():

In other words…

Because signInWithEmailAndPassword() is an asynchronous call, and we need to await for it to return, the next line is not executed within the test.

When running widget tests this is undesirable:

  • All code running inside our tests should be synchronous.
  • Widget/unit tests should run in isolation and not talk to the network.

Could we replace our call to Firebase with something we have control over, like a test mock?

Yes we can. 😎

Step 1. Let’s move our Firebase call inside an Auth class:

Note that we return a user id as String. This is so we don't leak Firebase types to the code using BaseAuth. Because the sign in is asynchronous, we wrap the result inside a Future.

Step 2. With this change, we can inject our Auth object when the LoginPage is created:

Note how our LoginPage holds a reference to the BaseAuth abstract class, rather than the concrete Auth version.

Step 3. We can create an AuthMock class for our tests:

Notes

  • The AuthMock.signIn() method returns immediately when called.
  • We can instrument our mock to return either a user id, or throw an error. This can be used to simulate a successful or failed response from Firebase.

With this setup we can write the last two tests, making sure to inject our mock when creating a LoginPage instance:

If we run our tests again, we now get a green light! ✅ Bingo! 🚀

Note: we can choose to write our expectations either on our mock object, or on the hintTextwidget. When writing widget tests, we should always be able to observe changes at the UI level.

Conclusion

When writing unit or widget tests, identify all the dependencies of your system under test (some of them may run code asynchronously). Then:

  • Inject dependencies as abstract classes into your widgets.
  • Instrument your tests with mocks and ensure they return immediately.
  • Write your expectations against the widgets or your mocks.
  • [Flutter specific] call tester.pump() to cause a rebuild on your widget under test.

Full source code is available on this GitHub repo. This includes a full user registration form in addition to the login form.

You’re welcome!

What is your experience with testing in Flutter? Let me know in the comments. 👇

References

For more articles and video tutorials, check out Coding With Flutter.

About me: I’m a freelance iOS developer, juggling between contract work, open source, side projects and blogging.

I’m @biz84 on Twitter. You can also see my GitHub page. Feedback, tweets, funny gifs, all welcome! My favourite? Lots of 👏👏👏. Oh, and banana bread.

--

--