Testing GraphQL Backend in Product Hunt
Tags: graphql, ruby, testing
Published: 2020-08-04 13:03:25

---
title: Testing GraphQL Backend in Product Hunt
published: true
description: Custom tooling and conventions about testing resolver and mutation GraphQL ruby classes.
tags: #graphql, #ruby, #testing
canonical_url: https://blog.rstankov.com/testing-graphql-backend-in-product-hunt/
---

People don't write tests because it takes too much time to write them. They lose a lot of mental energy on answering trivial questions like "Do we test this in our application?", "Should I test this?", "How do I test this?", "How do I set up my test", and so on.

For your team to write tests, your team members need two things: good conventions on how and what to test, and good tooling.

I spend a lot of time making the common use-cases easy to test. In [Product Hunt](http://producthunt.com/), one such example is our Ruby [GraphQL](https://graphql.org/) testing helpers.

We test three categories of GraphQL API code.

1. Helpers
2. Mutation classes
3. Resolver classes

### Helpers

Those are helper module and classes like `RecordLoader` / `RequestInfo` / `Context`  / `RateLimiter` and others. We don't have many of those. They are regular Ruby objects and are tested as such.

### Mutations

[Mutations in GraphQL](https://graphql.org/learn/queries/#mutations) are the operations that change your data. It is **very, very** important for those to be fully tested. 

For each mutation, we test:

* Authentication and authorization rules
* Failure cases - validation, errors
* Happy paths

Mutations inherit from [Mutations::BaseMutation](https://blog.rstankov.com/how-product-hunt-structures-graphql-mutations/#backendtooling) class. *I have written a blog post on the topic - [here](https://blog.rstankov.com/how-product-hunt-structures-graphql-mutations/#backendtooling)*

Here is an example mutation:

```ruby 
module Mutations
  class QuestionCreate < BaseMutation
    argument :title, String, required: false

    authorize :create, Question

    returns Types::QuestionType

    def perform(attributes)
      question = Question.new(user: current_user)
      question.update(attributes)
      question
    end
  end
end
```

The way to test this, without extra tooling is:

```ruby
describe Mutations::QuestionCreate do
  let(:user) { create :user }

  it 'creates a question' do
    context = Graph::Context.new(
      query: OpenStruct.new(schema: StacksSchema),
      values: context.merge(current_user: user),
      object: nil,
    )

    result = described_class.new(object: nil, context: context, field: nil).resolve(
      title: 'What email client do you use?',
    )

    expect(result[:errors]).to eq []
    expect(result[:node]).to have_attributes(
      id: be_present,
      user: user,
      title: 'What email client do you use?',
    )
  end
end
```

Yikes! 🥶 Having to write something like this for every mutation will put off even the most enthusiastic developers.

The [graphql-ruby](https://graphql-ruby.org/) classes aren't designed to be instantiated and run by you. The gem itself does this. Thus running them requires a lot of orchestration. That's why we have custom helpers to test mutations.

We have a [custom helpers](https://gist.github.com/RStankov/9de68e4138758fe2042f705764e3c58c) to test mutations.

Here is the updated example:

```ruby
describe Mutations::QuestionCreate do
  let(:user) { create :user }

  it 'requires an user' do
    # executes the mutation with one-liner
    result = execute_mutation(
      title: 'test',
    )

    # verify that mutation returned this error
    expect_access_denied_error(result)
  end

  it 'creates a question' do
    # the one-liner, allow us to specify current user
    result = execute_mutation(
      current_user: user,
      title: 'What email client do you use?',
    )

    # "expect_node" verify that:
    # 1. there are no errors
    # 2. "node" is not null
    expect_node(result) do |node|
      expect(node).to have_attributes(
        id: be_present,
        user: user,
        title: 'What email client do you use?',
      )
    end
  end

  it 'requires a title' do
    result = execute_mutation(
      current_user: user,
      title: '',
    )

    # "expect_error" verify that:
    # 1. "node" is nil
    # 2. there is an error for given attribute and message
    expect_error result, :title, :blank
  end
end
```

This is a lot better. It clearly communicates what the business logic of this mutation is.

[Here](https://gist.github.com/RStankov/9de68e4138758fe2042f705764e3c58c), you can find the [gist](https://gist.github.com/RStankov/9de68e4138758fe2042f705764e3c58c) of those helper methods.

### Resolvers

Resolvers inherit from [GraphQL::Schema::Resolver](https://graphql-ruby.org/fields/resolvers.html). They help us structure the code and make it easier to test. The alternative is to write the logic in the [type classes](https://graphql-ruby.org/type_definitions/objects.html#object-classes). We tend to avoid this because classes get bloated and are harder to test.

The following is an example of a resolver that tells us if the logged-in user has liked a given object:

```ruby 
class Resolvers::IsLiked < Resolvers::Base
  type Boolean, null: false

  def resolve
    current_user = context.current_user
    return false if current_user.blank?
    
    Graph::IsLoaderPolymorphic.for(current_user.likes).load(object)
  end
end
```

*Note: `Graph::IsLoaderPolymorphic` is batching helper. I have written about batching in GraphQL - [here](https://blog.rstankov.com/dealing-with-n-1-with-graphql-part-1/) and [here](https://blog.rstankov.com/dealing-with-n-1-in-graphql-part-2/)*.

How will we test this?  

We have two helpers for testing resolvers: `execute_resolver` and `execute_batch_resolver`. 

```ruby
describe Resolvers::IsLiked do
  let(:user) { create :user }
  let(:answer) { create :answer }

  def expect_call(object:, user:)
    expect(execute_batch_resolver(current_user: user, object: object))
  end

  it 'returns false when there is no user' do
    expect_call(object: answer, user: nil).to eq false
  end

  it 'returns false when the user has not liked the answer' do
    expect_call(object: answer, user: user).to eq false
  end

  it 'returns true when the user has liked the answer' do
    create: like, subject: answer, profile: user.profile

    expect_call(object: answer, user: user).to eq true
  end
end
```

[Here](https://gist.github.com/RStankov/9de68e4138758fe2042f705764e3c58c), you can find the [gist](https://gist.github.com/RStankov/9de68e4138758fe2042f705764e3c58c) of those helper methods.

### Conclusion

Test helpers can make testing more accessible. Our GraphQL helpers are just one example of this. People should think about the domain they are testing, not about test boilerplate.

If you have any questions or comments, you can ping me on [Twitter](https://twitter.com/rstankov).