0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RSpecで半自動でGraphQLの網羅テストを行う

Posted at

目的

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

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?