TDD GraphQL on Rails

Ruby on Rails with Rspec

Posted by Theo Cha on August 15, 2022

Let’s do TDD with GraphQL + Rails + Rspec

GraphQL is cool

Introduction


It has been a long time working with Rails + GraphQL technologies. so, I want to recap the Rails + GraphQL and summarize a few points by writing this public tutorial.

In this article, I will use the following technologies:

  • Ruby on Rails
  • GraphQL gem
  • Rspec

Getting started


Installation:

  • Install Rails and set up GraphQL gem by following this tutorial. we assume you create a Link model, controller, view, and records.

Focus points

  • TDD approach to create GraphQL.
  • Create queries feature.
  • Create mutations feature.

Queries


Setup specs

Run below spec file and yes, fail first.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require 'rails_helper'
RSpec.describe Link, type: :request do
  describe 'GraphQL query for looking up links' do
    it 'returns links' do
      Link.create(url: 'http://dev.apollodata.com/', description: 'Awesome GraphQL Client')
      query = <<-GRAPHQL
        query {
          allLinks {
            id
            url
          }
        }
      GRAPHQL
      post '/graphql', params: { query: query }

      json = JSON.parse(response.body)
      data = json['data']['allLinks']

      expect(data).to eq([{"id"=>"1", "url"=>"http://dev.apollodata.com/"}])
    end
  end
end

Here is a simple query field that returns Link records.

Setup Query type

1
2
3
4
5
6
7
8
9
10
11
12
module Types
  class QueryType < BaseObject
    # queries are just represented as fields
    # `all_links` is automatically camelcased to `allLinks`
    field :all_links, [LinkType], null: false

    # this method is invoked, when `all_link` fields is being resolved
    def all_links
      Link.all
    end
  end
end

Now, we run the spec file, all tests will pass.

Mutations


Setup a service

Before setting up the spec file, I want to decouple a creation logic from the GraphQL resolver to a service object because:

  • Thin Rails controller and GraphQL resolver: they are mainly responsible for understanding requests and responses.
  • Re-usable services
  • Testable: Since the logic in a controller or resolver is thinner and simpler, it becomes really easy to test. also, testing business processes becomes more isolated. it is easier to stub and check whether specific processes are invoked within our service.

If you are interested in how the service logic works, please visit Why do you need service objects

Let’s create a service spec file and fail first.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# `spec/services/link_create_spec.rb`
require 'rails_helper'
describe LinkCreate do
  it "creates a user and sends an email verification email" do
    result = nil

    expect {
      result = described_class.call(
        url: 'http://dev.apollodata.com/',
        description: 'Awesome GraphQL Client'
      )
    }.to change(Link, :count).by(1)

    list = Link.last!
    expect(result[:url]).to eq(list.url)
  end
end

Create a service object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class LinkCreate
  def self.call(...)
    new(...).call
  end

  def initialize(url:, description:)
    @url = url
    @description = description
  end

  def call
    link = Link.new(
      url: @url,
      description: @description
    )

    if link.save
      link
    else
      'fail'
    end
  end
end

Now, you run the spec file again and all test pass.

Setup mutations spec

We are going to create a spec file for the mutations. since we decoupled the creation logic from the Mutation’s resolver, we need to ensure whether the LinkCreate service is executed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#spec/graphql/mutations/create_link.rb
require "rails_helper"

RSpec.describe Mutations::CreateLink do
  describe  do
    it "calls LinkCreate service" do
      allow(LinkCreate).to receive(:call)
      query = <<-GRAPHQL
        mutation CreateLink($url: String!, $description: String!) {
          createLink(
            url: $url
            description: $description
          ) {
            id
            url
          }
        }
      GRAPHQL
      GraphqlTutorialSchema.execute(query, variables: {
        url: "test@example.com",
        description: "Example"
      })

      expect(LinkCreate).to have_received(:call).with(
        url: 'test@example.com',
        description: 'Example'
      )
    end
  end
end

Run the spec file and yes, fail first.

Adding Mutation field

Add the create_link filed at mutation_type.rb

1
2
3
4
5
6
#app/graphql/types/mutation_type.rb
module Types
  class MutationType < BaseObject
    field :create_link, mutation: Mutations::CreateLink
  end
end

And here is the create_link mutation file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#app/graphql/mutations/create_link.rb
module Mutations
  class CreateLink < BaseMutation

    argument :description, String, required: true
    argument :url, String, required: true

    type Types::LinkType

    def resolve(description: nil, url: nil)
      LinkCreate.call(
        url: url,
        description: description
      )
    end
  end
end

Now, run rspec spec/graphql/mutations/create_link.rb, then all test will pass.

References