目的
RSpec にて GraphQL の全てのフィールドを網羅的に半自動的に検証する。
※ あくまで網羅テストであり、各フィールドの細かい条件を検証するためのものではないです。
課題感
GraphQL は クエリストリングによって呼び出されるか が変わるため、GraphQL のフィールドを増減した際に都度 RSpec を変更する必要がある。
RESTFul API の場合は RSpec はそのままでも呼び出し時のエラーがないことは確認できる。
class Types::SampleType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :typo, String, null: false # object.typo というメソッドがない場合エラーになる
end
query GetSample1 {
sample {
id
name
}
}
# => 問題ない
query GetSample2 {
sample {
id
name
typo
}
}
# => エラーが発生する
How to
以下のライブラリを利用し、全てのフィールドを網羅したテストコードを自動生成する。
ただし、このライブラリは引数が足りていないものは検証しない。(必須の引数がある場合は引数を与えると自動生成される)
そのため GraphQL の引数の型を読み込んで適当な値を埋めるためのメソッドを用意し、引数ありの GraphQL に関しても検証できるようにする。
spec/support/graphql_arguments_fetcher_autofill.rb
module GraphqlArgumentsFetcherAutofill
def build_graphql_arguments(schema_class, field, &block)
# blockを受け取ってblock内で定義されているものがあればそちらを優先させる
block_result = block&.call(field) || {}
field.arguments.inject(block_result) { |member, input_value_definition|
member.reverse_merge(build_graphql_argument(schema_class, input_value_definition))
}
.presence
end
private
def build_graphql_argument(schema_class, input_value_definition)
{ input_value_definition.name.to_sym => value_of_graphql_argument(schema_class, input_value_definition.type) }
end
def value_of_graphql_argument(schema_class, node_type)
case node_type
when GraphQL::Language::Nodes::ListType
"[#{value_of_graphql_argument(schema_class, node_type.of_type)}]"
when GraphQL::Language::Nodes::NonNullType
value_of_graphql_argument(schema_class, node_type.of_type)
when GraphQL::Language::Nodes::TypeName
case node_type.name
when "String", "ID"
"\"#{SecureRandom.uuid}\""
when "Int"
rand(1..100)
when "Boolean"
[ true, false ].sample
when "ISO8601DateTime"
"\"#{Time.zone.now.iso8601}\""
else
detected_definition = schema_class.to_document.definitions.find do |definition|
definition.name == node_type.name
end
if is_enum_type?(detected_definition)
detected_definition.values.sample.name
else
args = detected_definition.fields.reduce({}) do |hash, input_value_definition|
hash.merge(build_graphql_argument(schema_class, input_value_definition))
end
"{#{args.map { |key, value| "#{key}: #{value}" }.join(",")}}"
end
end
else
raise "UnsupportedError with: #{node_type}"
end
end
def is_enum_type?(node_type)
node_type.is_a?(GraphQL::Language::Nodes::EnumTypeDefinition)
end
end
spec/graphql/query_spec.rb
require "rails_helper"
describe Types::QueryType do
before do
# fetchに必要なデータを用意
end
# ランダムな引数では意図したspecがかけない(詳細エンドポイントのuid...etc)場合はここで引数を注入
def required_arguments(field)
if field.name == "sample" && field.arguments.any? { |arg| arg.name == "uid" }
{ uid: sample.uid } # 固定化したい引数のみここで指定する
end
end
context "with 必須の引数のみ埋める" do
specify do
fill_first = proc do |field|
if field.arguments.present?
required_args = required_arguments(field)
if required_args.present?
required_args
else
args = field.arguments.filter { |arg| arg.type.is_a?(GraphQL::Language::Nodes::NonNullType) }.presence
# 今回はargumentを足したときに自動的にテストから外されることを避けるために例外にする
raise "argumentの指定が足りていません。field: #{field.name}。必須引数: #{args.map(&:name).join(', ')}" if args.present?
end
end
end
GraphQL::Autotest::Runner.new(
schema: SpecInternalAPISchema,
arguments_fetcher: GraphQL::Autotest::ArgumentsFetcher.combine(
fill_first,
GraphQL::Autotest::ArgumentsFetcher::DEFAULT
),
max_depth: 10,
context:
).report!
rescue GraphQL::ParseError => e
# デバッグ用
puts "GraphQL::ParseError"
puts e.query
raise
end
end
context "with 任意項目も全部渡す" do
specify do
fill_first = proc do |field|
build_graphql_arguments(SpecInternalAPISchema, field) do
required_arguments(field)
end
end
GraphQL::Autotest::Runner.new(
schema: SpecInternalAPISchema,
arguments_fetcher: GraphQL::Autotest::ArgumentsFetcher.combine(
fill_first,
GraphQL::Autotest::ArgumentsFetcher::DEFAULT
),
max_depth: 10,
context:
).report!
rescue GraphQL::ParseError => e
# デバッグ用
puts "GraphQL::ParseError"
puts e.query
raise
end
end
end
参考