There are a few ways to test the behavior of your GraphQL schema:
The easiest way to test behavior of a GraphQL schema is to extract behavior into separate objects and test those objects in isolation. For Rails, you don’t test your models by running controller tests, right? Similarly, you can test “lower-level” parts of the system on their own without running end-to-end tests.
For example, consider a field which calculates its own value:
PostType = GraphQL::ObjectType.define do
# ...
field :isTrending, types.Boolean do
resolve ->(obj, args, ctx) {
recent_comments = comments.where("created_at < ?", 1.day.ago)
recent_comments.count > 100
}
end
end
You can refactor this by creating a new class and applying it to your GraphQL schema:
# app/models/post/trending.rb
class Post
class Trending
TRENDING_COMMENTS_COUNT = 100
def initialize(post)
@post = post
end
def value
recent_comments = @post.comments.where("created_at < ?", 1.day.ago)
recent_comments.count > TRENDING_COMMENTS_COUNT
end
end
end
# ....
PostType = GraphQL::ObjectType.define do
# ...
field :isTrending, types.Boolean do
resolve ->(obj, args, ctx) {
# Use the Post::Trending class to calculate the value
Post::Trending.new(obj).value
}
end
end
This is an improvement because your behavior is not coupled to your GraphQL schema. Besides that, it’s easier to test: you can simply unit test the calculation class. For example:
# app/models/post/trending_spec.rb
describe Post::Trending do
let(:post) { create(:post) }
let(:trending) { Post::Trending.new(post) }
describe "#value" do
context "when there are no recent comments" do
it "is false" do
expect(trending.value).to eq(false)
end
end
context "when there are more than 100 recent comments" do
before do
101.times { create(:comment, post: post) }
end
it "is true" do
expect(trending.value).to eq(true)
end
end
end
end
You can access type and field objects from your schema to make sure they’re defined correctly and behave the way you expect them to.
Access a type by name from the schema with GraphQL::Schema#types
:
post = MySchema.types["Post"] # => PostType
post.fields # => {"id" => <GraphQL::Field>, ... }
post.fields.keys # => ["id", "title", "body", "author", "comments"]
The returned value of Schema#types[type_name]
is an instance of the type class you used to .define
it (eg, GraphQL::ObjectType
, GraphQL::EnumType
, GraphQL::InputObjectType
).
You can get a type’s fields from the GraphQL::ObjectType#fields
hash. For example:
post_type = MySchema.types["Post"]
title_field = post_type.fields["title"] #=> <GraphQL::Field>
title_field.name #=> "title"
You can test a resolve function by calling GraphQL::Field#resolve
:
# Because this field doesn't use context or variables, simply pass `nil`
post = Post.new(title: "Welcome to my blog")
name_field.resolve(post, nil, nil) #=> "Welcome to my blog"
Calling resolve
in this way does not apply any coercion. (That’s only applied during query execution.)
Similarly, you can access:
GraphQL::Field#arguments
, which are String
=> GraphQL::Argument
pairsGraphQL::Field#type
, the field’s return typeGraphQL::InputObjectType#arguments
, which are String
=> GraphQL::Argument
pairsGraphQL::EnumType#values
, which are String
=> GraphQL::EnumType::EnumValue
pairsGraphQL::Schema#possible_types(type_defn)
, which returns the possible types for union or interface types in a given schemaGraphQL::BaseType#unwrap
may also be helpful. It returns the “inner-most” type. For example:
required_list_of_strings = GraphQL::NonNullType.new(
of_type: GraphQL::ListType.new(
of_type: GraphQL::STRING_TYPE
)
)
required_list_of_strings.unwrap #=> GraphQL::STRING_TYPE
Sometimes, you really need an end-to-end test. Although it requires a lot of overhead, it’s nice to have a “sanity check” on the system as a whole.
You can execute queries on your schema in a test. For example, you can set it up like this:
describe MySchema do
# You can override `context` or `variables` in
# more specific scopes
let(:context) { {} }
let(:variables) { {} }
# Call `result` to execute the query
let(:result) {
res = MySchema.execute(
query_string,
context: context,
variables: variables
)
# Print any errors
if res["errors"]
pp res
end
res
}
describe "a specific query" do
# provide a query string for `result`
let(:query_string) { %|{ viewer { name } }| }
context "when there's no current user" do
it "is nil" do
# calling `result` executes the query
expect(result["data"]["viewer"]).to eq(nil)
end
end
context "when there's a current user" do
# override `context`
let(:context) {
{ current_user: User.new(name: "ABC") }
}
it "shows the user's name" do
user_name = result["data"]["viewer"]["name"]
expect(user_name).to eq("ABC")
end
end
end
end