Marvin Rabe

Testing GraphQL APIs with Laravel

I love functional testing in Laravel. It is quite easy to write useful tests in seconds. Lately I am switching from REST to GraphQL. But writing GQL in tests is a hassle. So I created some testing utilities to make the test driven development of GraphQL APIs easy again.

Even though I prefer Lighthouse as a GraphQL library, you are free to use any library you want. But I wholeheartedly recommend you to give Lighthouse a try. In my opinion it is the best option for Laravel.

Preconditions

In this article I will assume the following GraphQL schema:

query {
  account(id: ID!): Account!
  accounts: [Account!]!
}

type Account {
  id: ID!
  name: String!
  transactions: [Transaction!]!
}

type Transaction {
  id: ID!
  amount: Int!
}

I won’t go into detail on how to write GraphQL and I don’t explain how to implement the functionality in Laravel. My focus lies on writing good tests. If you dont have much experience with GraphQL I recommend you to read this tutorial on how to build a GraphQL Server with Laravel.

Calling GraphQL manually

First I will show you how to call a GraphQL endpoint manually. Later I introduce my testing library to make things much easier.

Oliver Nybroe has written an Article which explains the basics on Testing GraphQL in Laravel. By default GraphQL receives queries as JSON in a HTTP POST request. So we use the postJson method which comes with Laravel to send the queries manually.

Let us write our first simple test. I want to query a list of all accounts. Therefore I have to create accounts via factories and then query for them.

public function testAccounts() {
    $accounts = factory(Account::class, 2)->create();
    
    $this->postJson('graphql', [
          'query' => <<<GQL
            query {
                accounts {
                    id
                }
            }
          GQL
        ])
        ->assertJsonFragment(['id' => $accounts[0]->id])
        ->assertJsonFragment(['id' => $accounts[1]->id]);
}

Notice that you have to wrap your query string into an array with a query key.

Now on to our second test. I want to query a specific account with a list of all transactions.

public function testAccount() {
    $account = factory(Account::class)->create();
    $transactions = factory(Account::class, 2)->create([
        'account_id' => $account->id
    ]);
    
    $this->postJson('graphql', [
          'query' => <<<GQL
            query ($id: ID!) {
                account(id: $id) {
                    id
                    name
                    transactions {
                      id
                      amount
                    }
                }
            }
          GQL,
          'variables' => [
              'id' => $account->id
          ]
        ])
        ->assertJsonFragment(['name' => $account->name)
        ->assertJsonFragment([
            'id' => $transactions[0]->id
            'amount' => $transactions[0]->amount
        ])
        ->assertJsonFragment([
            'id' => $transactions[1]->id
            'amount' => $transactions[1]->amount
        ]);
}

Basically it has the same structure as the previous test. Did you notice the variables key? This is another GraphQL specific detail. It is good practice in GraphQL to avoid string manipulation for changing arguments. Instead pass them as variables alongside the query.

In my opinion writing tests this way has some problems. This approach requires you to write a lot of GQL strings. It may not be a problem in those simple tests, but when you have to write longer, nested queries it becomes a hassle. Take a look at the SQL ecosystem. We are accustomed to use query builders most of the time and only write SQL manually when it becomes too complex.

The second problem is that you have to know GraphQL specific details. You always have to write well formatted JSON post request manually. It becomes error prone and repetitive.

Therefore I have written some useful testing utilities that simplifies writing GraphQL tests.

Installing the Library

Lets install the library by calling composer:

composer require --dev marvinrabe/laravel-graphql-test

This library provides the TestGraphQL trait. Include it in your Laravel TestCase class or in every test case where you need GraphQL support.

<?php

namespace Tests;

abstract class TestCase extends BaseTestCase
{
    use MarvinRabe\LaravelGraphQLTest\TestGraphQL;

    // ...
}

For more details on how to set it up you might have a look at the github repository.

Refactoring our Tests

Assuming you have added the trait to your TestCase class. Every test now has a query and mutation method for calling your GraphQL server.

Using those methods to refactor the first test results in:

public function testAccounts() {
    $accounts = factory(Account::class, 2)->create();
    
    $this->query('accounts', ['id'])
        ->assertJsonFragment(['id' => $accounts[0]->id])
        ->assertJsonFragment(['id' => $accounts[1]->id]);
}

And the second test:

public function testAccount() {
    $account = factory(Account::class)->create();
    $transactions = factory(Account::class, 2)->create([
        'account_id' => $account->id
    ]);
    
    $this->query('account', ['id' => $account->id], ['id', 'name', 'transactions' => ['id', 'amount']])
        ->assertJsonFragment(['name' => $account->name)
        ->assertJsonFragment([
            'id' => $transactions[0]->id
            'amount' => $transactions[0]->amount
        ])
        ->assertJsonFragment([
            'id' => $transactions[1]->id
            'amount' => $transactions[1]->amount
        ]);
}

As you can see it reduces the line count drastically. You dont have to write huge strings. And you dont have to know any internal GraphQL details.

In my opinion this library is an enormous help.

Conclusions

I really love the ease of functional testing in Laravel. And with Libraries like Lighthouse it is really easy to create a GraphQL server. But the testing experience is subpar. With my small helper library it becomes much easier to write good functional GraphlQL tests in Laravel.