2

Unit testing a Flutter GraphQL app

 1 year ago
source link: https://medium.com/flutter-community/unit-testing-a-flutter-graphql-app-e7ab1e9d1a51
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Unit testing a Flutter GraphQL app

Hand drawn graph on dot paper

Photo by Isaac Smith on Unsplash

Writing tests is the basis of ensuring your application can be built (and modified) with speed an accuracy. It’s fundamental. But what happens when testing isn’t straightforward? When there are gotchas that end up slowing you down in the short-term?

That recently happened to me when switching over to the graphql_flutter package. It wasn’t easy to reason through setting up a proper test harness and I wanted to share my experience. Note: this isn’t going to be a tutorial on setting up the graphql_flutter package or the associated code generation and assumes the reader already has a project set up making use of them.

At the time of writing, the package versions I’ll be using are below. I used to use mockito exclusively, but the switch to null safety and the requirement for code generation moved me over to mocktail, which is amazing. I’m also a huge fan of Felix Angelov in general.

Requirements

Let’s lay down a few requirements for our testing harness:

  • We have a few areas in our app that use the GraphQLClient directly, which are mainly repository layers that live outside UI widgets
  • We also use the generated hooks in our UI widgets, though this is mainly isolated to queries
  • The same instance of the client should be available everywhere
  • Depending on the needs of the test, we should be able to define response data in a free-form manner, or with type-safety

Here are some things we know ahead of time:

  • We definitely don’t want actual network requests going out, that would introduce too much overhead
  • The direct GraphQLClient methods use a different execution path under the hood than the generated hooks (all right, you caught me, I didn’t know this ahead of time, I learned this as I was working on the harness)
  • The extension methods generated by the graphql_codegen package can’t be mocked directly with mocktail, which is unfortunate, but something that we can work around

Initial thoughts

While getting started, I first took a look at the tests in the graphql_flutter package itself. They’re mocking at a very low level (the HttpLink itself), which actually makes sense given they want to test that the core of the package is working as intended. This isn’t the best choice for our requirements (mainly type-safety), even if it does leave the internals of the package intact and mock free.

For our requirements, it’s going to be best to mock at as high a level as possible. Having direct control over the returned result will give us a huge amount of flexibility.

Helper implementation

Before we jump into our actual tests, we need to set up some helpers to manage boilerplate code for the harness. Everything that only needs to happen once and will be the same for every test.

I created a helpers.dart file in the root of my test directory to contain all of these, but you’re welcome to put them in the best spot for your project.

Client

One of our requirements is to use the same instance of the client everywhere. Whether we’re mocking direct client calls or calls from hooks, we don’t want duplicate work. The first thing we want to do is set up our GraphQLClient and ensure it has proper defaults. This includes mocking its QueryManager (that will allow us to handle mocking responses against hooks).

Take a look, then join me below where we’ll go over the pieces.

Client test harness

  • We’ll start at the bottom with lines 11-13. If you’re not familiar with the mocktail syntax, all this does is create classes we can mock from. Nothing fancy.
  • On lines 2–3 we create instances of these mock classes
  • Lines 5 is the most interesting! When mocking hook responses, especially against our code-generated classes and extension methods, some of the internals are still called. We want to make sure that our client can return defaultPolicies correctly.
  • Line 6 imply ensures we return our MockQueryManager from the client. This will be important in later implementations, but helps satisfy our requirement of not duplicating work and being able to set up everything in our test harness at the same time.

Query

Mocking queries is much more interesting! There are a few more concepts to understand, which I’ll outline below.

Query test harness

  • Line 13 sets up another mock class for us but allows it to accept a generic… this is important because mocktail matches in a type-aware manner
  • Line 15 is more mocktail specific syntax to allow us to match non-primitive any or captureAny argument matchers… also important that it accepts a generic
  • Line 1 gives us most of the magic. We accept a generic on generateMockQuery and then return the result as a MockQueryResult of the same exact generic! The generic not only allows us to know about the result, but also register the correct argument matchers and target the correct query response. Technically, you could use this function multiple times in the same test (but with different queries) and have the mocks work out just fine.
  • Line 4 generates the actual result that our queries will return
  • Line 5 tells the bare client to return said result when queried for
  • Lines 7–8 tell our query manager to do the same… this ensures that we see the same results whether we’re testing calls directly against the client or against hooks

Watch query

Watch queries are where it starts to get interesting! A lot more needs to be mocked, which is part of what makes the harness so helpful.

Watch query test harness

  • Lines 24–28 are more mocktail specific setup, but nothing we haven’t seen before
  • Lines 1–2 set things up similarly as well
  • Line 4 is where we start to diverge… as mentioned above, there’s more to mock and it all revolves around having access to a query object
  • Lines 7–15 mock out the bare implementation needed to support streams, which are the core of watch queries
  • Lines 16–19 do the same as our generateMockQuery function

Mutation

Honestly, there’s nothing special about mutations. They’re mostly a direct copy of the setup for query, but using different methods and mocks.

Mutation test harness

Summary

The above isn’t inclusive of all methods that would need test harnesses, but the basic pattern can be applied anywhere. Also, if we ever write a test and need to mock out more of the harness, we can do all of that in one place.

Direct client calls

Now that we have our test harness in place, let’s test direct client calls from our repository layer.

UserRepository

Before we go over the test, let’s see what the actual repository looks like. I’ve kept it to the basics for the sake of simplicity.

User repository

  • Lines 2–8 set up our repository with a GraphQLClient, either by injecting one directly (helpful for tests) or looking one up against our provider
  • Lines 11–18 set up our mutation… you can see the use of the generated classes and extension methods with mutate$CreateUser, Options$Mutation$CreateUser and Variables$Mutation$CreateUser
  • For lines 20–26 you could handle the response however you’d like… in this case I’ve decided to simply return the new user id from the repository (note that this error handling can and should be more fleshed out)

UserRepository test

Now that we know the repository method to be tested, we can actually write those tests. We’ll want to assert that:

  • The correct user id is returned from the method
  • The correct mutation was called just once
  • The mutation was called with the correct variables

Here’s how the test file shakes out. As usual we’ll break it down line-by-line below.

User repository test

  • Lines 2–6 set up our mockGraphQLClient via the helpers we’ve already established
  • Line 10 creates our mock result… note the Mutation$CreateUser class we’ve passed as the generic to generateMockMutation
  • Line 12 ensures we’re testing the success case
  • Lines 13–27 define the data that will be returned by the mutation, in this case, the user that was created. Note: we’ve used the generated classes here in order to ensure type safety on the return… we could have just as easily used Mutation$CreateUser.fromJson() and provided an object if we wanted to skip the verbose nature of the nested generated classes.
  • Line 29 creates the repository, injecting the mock client
  • Lines 30–31 call the method on the repository and assert that we get the correct user id from the response
  • Lines 33–35 verify that mutate was actually called on the client. Note: we provided the same Mutation$CreateUser class so the mutate call can be correctly identified.
  • Lines 36–46 verify that mutate was called with the correct options

UI widgets using hooks

Now it’s time to test our widgets that use hooks! We looked at the UserRepository above, so let’s look at a widget that would display information for a given user.

UserView

There’s nothing spectacular here, but we’ll go through it anyway.

User view

  • Lines 8–16 use the generated hook, userQuery$UserById, to query the user information we want to display
  • Lines 19–25 handle, very simply, some of our possible states
  • Lines 38–53 split out rendering the user data into a new widget, mainly just to keep things organized and easy to reason through

UserView test

Testing this widget, we’ll want to assert that:

  • A loading indicator displays when the result is loading
  • An error message displays when the result has an error
  • The appropriate elements render on screen when the result is successful

User view test

  • Lines 2–6 set up our client, just like in the repository test
  • We start to differ on lines 8–17 when we provide a function to set up our test scaffold that will be called during each test… we’re rendering the basic widgets needed around our UserView component such that it renders without error

The first test (the loading indicator) starts on line 20

  • Lines 22–23 create the required response (note passing Query$UserById as the generic) and ensure it will report that it’s loading
  • On line 25 we render the test scaffold
  • On line 27 we assert that we find a progress indicator on screen

The second test (the error message) starts on line 30

  • Similar to the above, we create the required response on lines 31–33… this time the response is not loading and instead has an error
  • On lines 35–38 we render the scaffold and make sure there’s an error message on screen

The last test (success) starts on line 41

  • Similar to both other tests, we create the required response on lines 42–44… this time the response is not loading and does not have an error
  • Instead, on line 45 we specify the data to be returned (similar to the direct client calls, we can use generated classes or the Query$UserById.fromJson() method)
  • On lines 57–60 we render the scaffold and make sure the display name and email are on screen

Recap

This one has honestly been a blast. Don’t get me wrong, it’s always nice when testing is easy and straightforward... but every once in a while there’s an extra challenge with a particular test harness that’s just super-satisfying to solve.

Hopefully this has been of some help. Happy testing, everyone!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK