GraphQLのクエリを自動的にテストする

こんにちは。株式会社ビットジャーニーでエンジニアをしてる@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](https://github.com/rmosolgo/graphql-ruby)を使っている場合にのみ可能です。Ruby以外の言語でGraphQLサーバーが実装されている場合は、生成されたクエリを実行する部分は別に実装する必要があります。







# 使い方


では、使い方を見ていきましょう。


## Installation

まずこのgemをインストールします。

```bash
$ gem install graphql-autotest
```


もしくはBundlerを使っている場合、次のコードを`Gemfile`に足してください。


```ruby:Gemfile
gem 'graphql-autotest'
```


## 設定例


### graphql gemを使っている場合

まずは、graphql gemを使っている場合の一番シンプルなコードを紹介します。


```ruby
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`に保存されている場合、次のように書くとクエリを生成できます。

```ruby
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の定義は次のようになります。

```ruby
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な値を返せばそのフィールドはスキップされます。


```ruby
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です。この値は適当です。



```ruby
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サーバーのテストを実施していただければ幸いです。