Testing GraphQL using RSpec
Related videos
In this lesson, we're diving into how to effectively test your GraphQL Server using RSpec. Prepare for a detailed exploration on how to run queries and mutations against the schema, making assertions on the GraphQL response, and extracting queries with variables for a dry test setup.
Understanding Your Test Environment
We'll start by taking a look at the fixtures. These live under spec/fixtures
where a fixture for accounts has been created. This setup injects a test account with a password into the database automatically, perfect for running tests. To make this happen, we need to set config.global_fixtures: :all
inside the rails_helper.rb
file. This config ensures Rails looks for anything in the fixtures folder and loads them all.
Moving on, let's take a quick journey to the app/controllers
, specifically the GraphQLController
. If you remember, this was set up by the GraphQL-Ruby gem. Here, you'll see a line that reads CoinfusionSchema
. Coinfusion
is the name of our example app. Depending on your setup, your application name will live here. The execute
function indicates where your query gets passed in.
CoinfusionSchema.execute(query)
Though there might also be variables and context, we will circle back to these later on. For now, it's important to understand this fundamental aspect – that every query hits this endpoint.
Writing Tests
We'll now head over to our first test. For our first test, namely returns a token
, we'll make use of the ruby here doc
syntax for multiline strings.
query = <<~GQL
mutation {
login (input: {
email: "test@example.com",
password: "bad"
})
{
token
}
}
GQL
We'll replace hardcoded data with actual user credentials, and using CoinfusionSchema.execute
with the query as a parameter should work just fine.
Let's test this by printing out the result:
# spec/graphql/mutations/login_spec.rb
require "rails_helper"
RSpec.describe "mutation login" do
it "authenticates the account returning a token" do
query = <<~GQL
mutation {
login (input: {
email: "test@example.com",
password: "hunter2"
})
{
token
}
}
GQL
result = Coinfusion::Schema.execute(query: query)
puts result
end
end
The output of this should be a GraphQL query result
, similar to a Hash in Ruby. Specifically, a hash containing a login
field and a nested token
field. This is the data we're after. Next, let's test that these fields exist and that a token is returned.
query = <<~GQL
mutation {
login (input: {
email: "test@example.com",
password: "hunter2"
})
{
token
}
}
GQL
result = Coinfusion::Schema.execute(query: query)
expect(result.dig("data", "login", "token")).not_to be_blank
Nice. As this is testing for successful login, how about the inverse? Let's mimic a login failure by providing an invalid password. We should be able to test that the result is nil
, and ensure the appropriate error message is returned.
it "returns an error when authentication fails" do
query = <<~GQL
mutation {
login (input: {
email: "test@example.com",
password: "bad"
})
{
token
}
}
GQL
result = Coinfusion::Schema.execute(query: query)
expect(result["data"]).to be_nil
expect(result["errors"].first["message"]).to eq("Invalid email or password")
end
Running both tests should be successful.
DRYing up the tests
We've got working tests, but there's a lot of duplication here with our GraphQL query. Let's extract this and make it a method named login_query
.
Variables come in handy here for reusability. We can name our mutations and queries, and we more or less stick to whatever they are called initially. Setting up the variable names and marking them as required fields allows us to substitute them in the query instead of the hardcoded email and password values. This DRYs up the code and compacts our login query from multiple lines in every test to a singular method call with the necessary data parsed in.
def login_query
<<~GQL
mutation Login($email: String!, $password: String!) {
login(
email: $email
password: $password
) {
token
}
}
GQL
end
And, with our tested data now stored in variables, we can pass it to the login_query inside each test.
it "authenticates the account returning a token" do
result = CoinfusionSchema.execute(login_query, variables: {
email: "test@example.com",
password: "hunter2"
})
expect(result.dig("data", "login", "token")).not_to be_blank
end
it "returns an error when authentication fails" do
result = CoinfusionSchema.execute(login_query, variables: {
email: "test@example.com",
password: "bad"
})
expect(result["data"]).to be_nil
expect(result["errors"].first["message"]).to eq("Invalid email or password")
end
Rerunning these tests should lead to the same results as before.
Writing Registration Tests
Finally, we're going to port the above logic to our register_spec
. Starting with the returns error and validation fields
spec, we can construct a registration query that will be supplied for our tests. Unlike the login_query
this one takes a lot more options.
def register_query
<<~GQL
mutation Register($accountType: AccountType!, $email: String!, $password: String!, $name: String!, $dateOfBirth: String!) {
register(
accountType: $accountType,
email: $email,
password: $password,
name: $name,
dateOfBirth: $dateOfBirth
) {
name
email
token
}
}
GQL
end
We can then write our first test to handle when registration fails:
it "returns an error when validation fails" do
result = CoinfusionSchema.execute(register_query, variables: {
accountType: "individual",
email: "test@example.com",
password: "",
name: "Test",
dateOfBirth: "1990-01-01"
})
expect(result["errors"].first["message"]).to eq("Register failed.")
end
For the successful registration test, we need a valid password. Following this, we can then test and compare that the registered user's name and email match the input supplied, and that a token is returned.
it "creates a new account returning a token" do
result = CoinfusionSchema.execute(register_query, variables: {
accountType: "individual",
email: "test@example.com",
password: "long-password!",
name: "Test",
dateOfBirth: "1990-01-01"
})
expect(result.dig("data", "register", "email")).to eq("test@example.com")
expect(result.dig("data", "register", "name")).to eq("Test")
expect(result.dig("data", "register", "token")).not_to be_blank
end
And with that, our registration tests are working.
Implementing tests for the Me query
This query retrieves information about the currently logged-in user. This is where we'll make use of the context
for GraphQL, allowing us to pass in an account
.
# spec/graphql/queries/me_spec.rb
require "rails_helper"
RSpec.describe "query me" do
let(:account) { accounts(:test) }
it "returns info about the current logged in account" do
query = <<~GQL
query {
me {
id
name
email
}
}
GQL
result = CoinfusionSchema.execute(query, context: { current_account: account })
expect(result.dig("data", "me", "id")).to eq(String(account.id))
expect(result.dig("data", "me", "name")).to eq("Test Account")
expect(result.dig("data", "me", "email")).to eq("test@example.com")
end
end
Running this test should be successful. Here, we pass in the current_account
or any object that's relevant to your GraphQL context directly via the keys of the context
param. This is a very efficient way to pass context-specific information into your tests.
To wrap up, let's run all of the GraphQL tests together and confirm that all are passing. We can do this by running bin/rspec spec/graphql
and voilà! The tests are all green.
Conclusion
I hope that this lesson has been a comprehensive guide to testing your GraphQL server with RSpec. Testing is an integral part of development and being able to effectively ascertain the reliability of your GraphQL server is an invaluable skill.
Please don't hesitate to drop your comments, questions, or any topics you would like to know more about GraphQL. Happy coding!