こんにちは。株式会社ビットジャーニーでエンジニアをしてる@pockeです。
ビットジャーニーではKibelaというWebアプリケーションを開発しています。
KibelaではPublicなWeb APIにGraphQLを使用して提供しています。
https://github.com/kibela/kibela-api-v1-document
また、このAPIはKibelaのWebアプリケーションの内部でも同じものが使用されています。
先日、このGraphQL APIを自動的に(ある程度)網羅的にテストするためのgem graphql-autotestをリリースしました。
https://github.com/bitjourney/graphql-autotest
この記事ではこのgraphql-autotestを紹介します。
なおgraphql-autotestはRubyで書かれていますが、任意の言語で実装されたGraphQLサーバーをテスト可能です(Rubyのコードを多少書く必要はありますが)。
「網羅的なテスト」とは
graphql-autotestの目的は、GraphQLのクエリをある程度網羅的にテストすることです。
私達は普段GraphQL APIを開発する際にユニットテストを書いています。
理想論を言えばすべてのフィールドにユニットテストを書きたいところですが、しばしば漏れが生じます。
graphql-autotestはそのような漏れを拾い上げるようなテストです。
まず、スキーマ定義から可能な限りすべてのフィールドを取得するようなクエリを自動的に書き出します。
そしてそのクエリを実行し、エラーが出ないことを確認します。
ポイントは、クエリを自動的に書き出す点です。
クエリの生成を自動化することで、新規に追加したフィールドにユニットテストを書き忘れていたとしても、graphql-autotestでそのフィールドをテストできます。
ただし、デフォルトではgraphql-autotestはクエリの実行がエラーを出さないことのみをチェックします。
「GraphQLサーバーの実装にtypoがあってNoMethodErrorが出ていた」と言ったケースは検出できますが、「1が返るべきところで2が返っていた」と言ったバグは検出できません。
ただし、クエリを実行したレスポンスに対して、それを検証する適当なコードを書いてやればある程度はテストできるでしょう。
なお、クエリの実行はgraphql gemを使っている場合にのみ可能です。Ruby以外の言語でGraphQLサーバーが実装されている場合は、生成されたクエリを実行する部分は別に実装する必要があります。
使い方
では、使い方を見ていきましょう。
Installation
まずこのgemをインストールします。
$ gem install graphql-autotest
もしくはBundlerを使っている場合、次のコードをGemfile
に足してください。
gem 'graphql-autotest'
設定例
graphql gemを使っている場合
まずは、graphql gemを使っている場合の一番シンプルなコードを紹介します。
require 'graphql/autotest'
class YourSchema < GraphQL::Schema
end
runner = GraphQL::Autotest::Runner.new(
schema: YourSchema,
context: { current_user: User.first },
)
runner.report!
まず、YourSchema
としてスキーマ定義をしています。
これは自身のスキーマ定義に置き換えてください。
そして、GraphQL::Autotest::Runner
のインスタンスを生成し、report!
でクエリを生成、実行しています。
Runner.new
の引数には定義したYourSchema
と、context
を渡しています。このcontext
はYourSchema.execute
を呼ぶ際に渡されます。
これだけでスキーマ定義からクエリを自動生成して、実行し、エラーがないかをテストできます。
ただし、実用上ではデフォルトの設定では充分な結果が得られないことがあるでしょう。そのため後述するarguments_fetcher
などの設定が必要です。
graphql gemを使っていない場合
冒頭でも説明したとおり、このgemはgraphql gemを使ってサーバーを実装している場合以外でもクエリの自動生成ができます(クエリの実行はできません)。
クエリを自動生成するには、 https://github.com/kibela/kibela-api-v1-document/blob/master/schema.graphql のようなスキーマ定義が必要です。
このスキーマ定義がpath/to/schema.graphql
に保存されている場合、次のように書くとクエリを生成できます。
require 'graphql/autotest'
fields = GraphQL::Autotest::QueryGenerator.from_file(path: 'path/to/schema.graphql')
# 生成したクエリがすべて表示される
fields.each do |field|
puts field.to_query
end
生成したクエリは適応なGraphQLクライアントに渡して実行すると良いでしょう。
設定方法
ここまでで、デフォルトの設定でクエリを生成、実行する方法を解説しました。
ですが前述したとおり、デフォルトの設定では不十分なケースもあります(Kibelaの場合はそうです)。
そのためgraphql-autotestではいくつかの設定項目を用意しています。
以下にその設定項目を解説します。
arguments_fetcher
arguments_fetcher
では、フィールドが引数を受け取る場合にどのような引数を渡すかを定義します。
arguments_fetcher
はProcを指定します。指定したProcはフィールドの情報を引数として呼ばれ、Hashを返せばそれが引数として使われます。
デフォルトではgraphql-autotestでは必須の引数がない(== 引数なしで呼べる)フィールドのみをクエリとして書き出します。
そのため、必須の引数があるフィールドを取得するためにはarguments_fetcher
を使用して、どのような引数を渡したら良いかを定義します。
また必須でない引数でも、必要があれば指定できます。
たとえば、KibelaではRelay Connectionに対するfirst
かlast
引数が必須となっています。
このfirst
を埋めるようなfetch_argumentsの定義は次のようになります。
fill_first = proc do |field|
field.arguments.any? { |arg| arg.name == 'first' } && { first: 5 }
end
fields = GraphQL::Autotest::QueryGenerator.from_file(
path: 'path/to/schema.graphql',
arguments_fetcher: GraphQL::Autotest::ArgumentsFetcher.combine(
fill_first,
GraphQL::Autotest::ArgumentsFetcher::DEFAULT,
),
)
このarguments_fetcher
をより設定することで、フィールドの網羅性を上げることができます。
skip_if
skip_if
では、取得したくないフィールドをスキップできます。
どうしても自動的なクエリの生成ではエラーが出てしまうようなフィールドを無視するのに使えるでしょう。
skip_if
もProcを指定します。このProcはフィールドの情報を引数に呼ばれ、truthyな値を返せばそのフィールドはスキップされます。
skip_if = proc do |field|
field.name == 'sensitiveField'
end
fields = GraphQL::Autotest::QueryGenerator.from_file(
path: 'path/to/schema.graphql',
skip_if: skip_if,
)
max_depth
max_depth
には、クエリの最大の深さを指定します。
graphql-autotestではフィールドの循環参照があった場合に無限ループにならないよう制御しているため、理論上はmax_depth
を指定しなくてもクエリの実行は終了します。
とはいえクエリが深すぎる場合、クエリの生成や実行に時間がかかってしまいます。
その場合はmax_depth
オプションを使用して、深さを減らしてください。
デフォルトのmax_depth
は10です。この値は適当です。
fields = GraphQL::Autotest::QueryGenerator.from_file(
path: 'path/to/schema.graphql',
max_depth: 5,
)
まとめ
GraphQLの網羅的なテストを実行するためのGemであるgraphql-autotestを紹介しました。
実際にKibelaではこのgemを実装する過程で複数のバグを発見しました。
バグがあったのはいずれもKibelaのWebアプリケーションでは使っていないフィールドでした。ですが、ユーザーに公開されているAPIでも取得されうるフィールドであることを考えると早めにバグを発見できたのは良かったと思います。
graphql-autotestを使って、GraphQLサーバーのテストを実施していただければ幸いです。