Tech & Design LAB

Jsonnetを使ったメンテナンス可能なGraphQLのAPIテストの作成方法

#バックエンド #Jsonnet #GraphQL #技術選定
author icon
Posted on
tech

小田です。最近twitterをまた始めました。渋谷区の「LINEで住民票請求」関連で激しく燃えているのを観測しております。かなり偉い人を巻き込んで赤く燃えていましたね。個人的には、利便性のために認証方式をいじるよりもマイナンバーのエコシステムの利便性を高めてほしいと思っています。xID社の以下のような取り組みはめっちゃ気になっていて、もしブロッキングが広報であるなら政府は凄腕マーケターを雇い、B向けマーケに資金を投じてほしいです。新しくゴニョゴニョやるより安いでしょ。

Postmanのコレクションのメンテナンス性について

本題ですが、今日はみんな大好きGraphQLのテストについてです。弊社では以下の方針でGraphQLのテストを書いてます。https://shinh.hatenablog.com/entry/2019/09/12/201335 に触発されて細かいユニットを捨ててE2Eによってきていますね。ちなみにここで言うE2EテストはAPIのE2Eテストになります。TestimPlaywrightのようなエンドユーザを想定したE2Eテストとは異なります。どうでもいいですが、Testimいいですよね。

Postmanのテストを書かれている方はわかると思うんですが、まあメンテナンスが難しいですね。最初は頑張ってやるんですが、以下の問題で更新されずだんだん廃れていきます。

Postman Jsonnetについて

主な原因はJSONの構造やJSONのメンテンナス性にあるので、弊社ではテストケースをJsonnetを使用して記述し、実行時は出力したJSONをNewman(Postmanのcli)で実行しています。Jsonnetは「A powerful DSL for elegant description of JSON data」と位置づけられたテンプレート言語で、かなりメンテナンス性が高いです。単純なリソースを作成するテストケースは以下のように記述して実行できます。

テストケースの概要は以下:

対応するテストケースは以下:

local test = import 'postman.libsonnet';

test.suite {
  local createCustomer = |||
    mutation createCustomer($input: CustomerInput!) {
      createCustomer(input: { customer: $input }) {
        customer {
          id
          name
        }
      }
    }
  |||,

  name: 'customer',
  item: [
    test.graphql(createCustomer, { input: { name: 'c00' } }),
    test.graphql(createCustomer, { input: { name: 'c01' } }) + {
      tests+: [
        |||
          const d = pm.response.json().data;
          pm.expect(d).to.exist;
          pm.expect(d.createCustomer.customer.name).to.equal("c01");
        |||,
      ],
    },
  ] + [
    test.graphql(createCustomer, { input: { name: 'c%02d' % x } }) + {
      tests+: [
        |||
          const d = pm.response.json().data;
          pm.expect(d).to.exist;
          pm.expect(d.createCustomer.customer.name).to.equal("%02d");
        ||| % [x],
      ],
    }
    for x in std.range(2, 10)
  ],
}

上記で作成したテストケース(collection.jsonnetとします)は以下で実行できます。

go get github.com/google/go-jsonnet/cmd/jsonnet                                    # Setup
go get github.com/google/go-jsonnet/cmd/jsonnetfmt                                 # Setup
git clone https://github.com/akamai-contrib/postman-jsonnet vendor/postman-jsonnet # Install lib

jsonnet -J vendor -o collection.json collection.jsonnet                            # Build
newman run collection.json --verbose                                               # Run

いかがでしょうか?シンプルでなおかつ複雑なケースにも対応できるイメージがわきませんか?弊社ではnewman + Jsonnetによって多くの時間を節約しています。利点と欠点をまとめると以下になります。

利点は以下:

欠点は以下:

Jsonnetの拡張性と雑多な話

実は上記の例はそのままでは動かなくて、Jsonnetのライブラリとして https://github.com/akamai-contrib/postman-jsonnet をラップした postman.libsonnet の実装を追加する必要があります。追加した機能は以下の2つになります。この2つの機能をこれだけシンプルに追加できるのは、Jsonnetの遅延評価とオブジェクト指向の性質によります。このエレガントさが気になった方は次のリンク先をご確認ください。 https://jsonnet.org/learning/tutorial.html

local test = import 'postman-jsonnet/postman.libsonnet';
local scope = import 'postman-jsonnet/src/scope.libsonnet';

(test) + {
  case:: scope.EventScope + scope.VariableScope + {
    event: if !std.extVar('debug') then super.event else [
      {
        listen: 'test',
        script: {
          exec: |||
            (() => {
              console.log("Request Headers: ", pm.request.headers);
              console.log("Request Body: ", pm.request.body);
              console.log("Response Headers: ", pm.response.headers);

              if (pm.response.headers.get('Content-Type') !== 'application/json') { return; };
              console.log("Response Body: ", pm.response.json());
            })();
          |||,
        },
        type: 'text/javascript',
      },
    ] + super.event,
  },

  graphql(query, variable={})::
    test.case {
      name: query,
      request: test.POST('{{api_private_url}}/graphql') +
               test.header('Accept', 'application/json') +
               test.header('Authorization', 'Bearer {{api_private_token}}') +
               test.body.graphql({ query: query, variable: variable }),
      tests: [
        test.assertStatusCodeEquals('test.assertStatusCodeEquals', 200),
      ],
    },
}

まああと実は以下の所々のファイルも必要です。

function(
  api_private_url,
  api_private_token,
) {
  environment: {
    id: 'env',
    name: 'env',
    values: [
      // GraphQLのURLを指定
      { key: 'api_private_url', value: api_private_url, enabled: true },

      // GraphQLの認証で使用するtokenを指定
      { key: 'api_private_token', value: api_private_token, enabled: true },
    ],
  },
}
setup_env:
	jsonnet -J vendor -J libsonnet -o env.json env.jsonnet \
		--tla-str api_private_token=xxx \
		--tla-str api_private_url=xxx

clean:
	rm *.json

build:
	jsonnet -J vendor -J libsonnet -o collection.json collection.jsonnet --ext-code debug=true

run: build
	npx newman run collection.json -e env.json --verbose --bail

最終的には以下で実行できます

make run

お仕事一緒にしませんか?

どうでしょうか?読んでる方にとって、少しでもなにか得るものがあれば幸いです。唐突ですがこの内容に少しでも興味を持った方は一緒にお仕事をしてみませんか?フリーランスの方や副業前提の方もまずはカジュアル面談でも😉 仕事内容の確認やご応募は次のリンクからお願いします。 https://hicustomer.notion.site/HiCustomer-0d4844ffdcd046e994fe69c3ff787a03

author icon
シニアエンジニア

最近はAWS CDKとAWSのdata lake周りのソリューションが好きです🙋‍♀️よく、lwn.netとCrypto-Gram Newsletterを結構みています。djbと及川さんを尊敬しています。