どうも、じゃがです!
本記事は AWS Amplify Advent Calendar 2020 の9日目のエントリになります
2020年はAmplify iOS, AndroidのGA、Amplify JSのSSR対応やFlutterのDeveloper Preview、そしてAmplify Admin UIの登場など、Amplifyのアップデートが楽しい一年でしたね!
今回はGraphQL APIのユニットテストを書くノウハウについてまとめてみたいと思います!
想定読者
- AmplifyでGraphQL APIのテストを書きたい人
- テストが要件に入ってくる開発でもAmplify使いたい人
Amplify の基本的な使い方については解説しませんのでご了承ください。手を動かしてAmplify学びたいとうかたは、以下のWorkshopもご参照ください
動作確認環境
- @aws-amplify/cli v4.34.0
- 本記事で作成したコード: https://github.com/jaga810/amplify-graphql-test
GraphQL APIのテストとは?
Amplify CLIでGraphQL APIを構築する際、Amplify Mockingを用いてAPIレスポンスが期待通り返ってくるか、手元で試しながら実装される方は多いのではないでしょうか。(Amplify Mockingを使ったことがないという方はぜひ[GraphQL API開発スピードを爆上げするAWS Amplify Mockingことはじめ] (https://qiita.com/nagym/items/638974a3a5aaa63841c8)をご参照ください)
一方で開発中、継続的にユニットテストを実行したいというニーズもあるかと思います。ユニットテストがあれば今まで実装した部分が新たなコードの変更で崩れないことを担保でき、開発のスピードも質も向上します
Amplify CLIのGraphQL APIでユニットテストはどのように実施すればよいでしょうか?
本記事ではJavaScriptテスティングフレームワークのJestを用いて、Amplify Mockingで立ち上げたローカルサーバーへGraphQL Operationを行い、挙動をテストするという方針でユニットテストを行います
また、そのテストをAmplify Consoleを利用したCI/CDパイプラインで動かします
GraphQL APIの作成
Amplify プロジェクトの初期化
#適当な作業ディレクトリで以下のコマンドを実行します
$ mkdir graphql-test; cd $_
$ amplify init
#amplify initで聞かれる項目は全てデフォルトの選択肢で大丈夫です
GraphQL API の作成
$ amplify add api
#amplify add apiで聞かれる項目は、認証方法でCognitoを選ぶこと以外は全てデフォルトの選択肢で回答します
シンプルなスキーマを持つ Todo モデルが出来上がりました
type Todo @model {
id: ID!
name: String!
description: String
}
認可の仕組みを実装するために、このスキーマを以下のように書き換えます
type Todo
@model
@auth(
rules: [
{allow: owner, ownerField: "owner", operations: [create, read, update, delete]},
]
)
{
id: ID!
name: String!
description: String
}
これにより最初にTodoを作成したowner
だけが、read/update/delete処理を実行できるようになりました
動作確認
ユニットテストではローカル環境でGraphQL APIの動作確認が可能なAmplify Mockingを利用します。Amplify Mockingは内部的に、amplify-appsync-simulatorと、DynamoDB Localを利用しています
$ amplify mock api
#amplify mock apiで聞かれる項目はすべてデフォルトの選択肢で回答します
...
AppSync Mock endpoint is running at http://192.168.1.2:20002 (http://192.168.1.2:20002/)
最後に表示されたエンドポイントでamplify-appsync-simulatorが立ち上がっており、アクセスすると以下のようなAmplify GraphiQL Explorerが表示されます
ユニットテストの実行
Jestの導入
まずはpackage.json
を作成します
$ npm init
#test commandのみ jest と入力しましょう。$ npm testコマンドでjestが走るようになります
Jestをはじめとしたテストに必要なライブラリをインストールします
$ npm install —save-dev jest babel-jest babel-plugin-transform-es2015-modules-commonjs
また、プロジェクト直下に以下のファイルを作成します
{
"env": {
"test": {
"plugins": [
"transform-es2015-modules-commonjs"
]
}
}
}
module.exports = {
transform: {
'^.+\\.js$' : '<rootDir>/node_modules/babel-jest',
},
moduleFileExtensions: ['js']
}
@authのテストを書く
@authで指定した通りに、ownerしかupdateができないことを確認するユニットテストを書いていきます
テスト内で必要なライブラリをインストールします
npm install —save-dev graphql-request graphql crypto base64url
Amplify JavaScriptのライブラリはAppSyncに渡すヘッダを自由に変更できないので、軽量なgraphql-requestをGraphQLクライアントとして利用します
cryptoとbase64urlはCognitoのJWTトークンを擬似的に生成するために利用します
プロジェクト直下に以下のファイルを作成します
import { GraphQLClient } from 'graphql-request';
import crypto from 'crypto';
import base64url from 'base64url';
import { createTodo, updateTodo } from './src/graphql/mutations';
const cognitoJwtGenerator = ({username}) => {
const header = {
'alg': 'HS256',
'typ': 'JWT'
}
const payload = {
'sub': '7d8ca528-4931-4254-9273-ea5ee853f271',
'cognito:groups': [],
'email_verified': true,
'algorithm': 'HS256',
'iss': 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_fake_idp',
'phone_number_verified': true,
'cognito:username': username,
'cognito:roles': [],
'aud': '2hifa096b3a24mvm3phskuaqi3',
'event_id': '18f4067e-9985-4eae-9f33-f45f495470d0',
'token_use': 'id',
'phone_number': '+12062062016',
'exp': 16073469193,
'email': 'user@domain.com',
'auth_time': 1586740073,
'iat': 1586740073
}
const encodedHeaderPlusPayload = base64url(JSON.stringify(header)) + '.' + base64url(JSON.stringify(payload));
const hmac = crypto.createHmac('sha256', 'secretKey')
hmac.update(encodedHeaderPlusPayload)
return encodedHeaderPlusPayload + '.' + hmac.digest('hex');
}
//2ユーザーからリクエストを行えるよう2つのクライアントを作成
const testUsers = ['user_0', 'user_1'];
const clients = [];
clients.push(new GraphQLClient('http://localhost:20002/graphql', {
headers: {
Authorization: cognitoJwtGenerator({username: testUsers[0]})
},
}));
clients.push(new GraphQLClient('http://localhost:20002/graphql', {
headers: {
Authorization: cognitoJwtGenerator({username: testUsers[1]})
},
}));
describe('Todo Model', () => {
test('Only owner can update their todos', async () => {
const testTodo = {
name: 'Test task',
description: 'This is a test task for unit test',
};
// Test用Todoの作成
const created = await clients[0].request(createTodo, {input: testTodo});
// Owner自身によるUpdateが成功することを確認
const updatedName = 'Updated Test Task by user_1';
const updatedByOwner = await clients[0].request(updateTodo, {input: {id: created.createTodo.id, name: updatedName}});
expect(updatedByOwner.updateTodo.name).toStrictEqual(updatedName);
// Owner以外によるUpdateが失敗することを確認
const updatedByOthers = clients[1].request(updateTodo, {input: {id: created.createTodo.id, name: ''}});
await expect(updatedByOthers).rejects.toThrowError('ConditionalCheckFailedException');
});
});
テストを実行してみましょう。( $ amplify mock api が動いていることを確認してください)
$ npm run test
graphql-test@1.0.0 test /Users/daisnaga/Dev/amplify-playground/graphql-test
> jest
PASS ./auth.test.js
Todo Model
✓ Only owner can update their todos (166 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.65 s
Ran all test suites.
テストが通りました!
CI/CDパイプライン (Amplify Console) でGraphQL APIのユニットテストを実行する
1コマンドでAppSync Simulatorの起動からテストの実行までを行う
これまでのように$ amplify mock api
を起動しておきつつ、別のシェルで$ npm run test
を実行するのは、手元ではAppSync Simulatorの開始処理を何度も行わないで済むというメリットがある一方、Amplify ConsoleなどCI/CDパイプラインを利用する際は不便です。1コマンドでAppSync Simulatorの起動からテストの実行までを行うため、bahmutov/start-server-and-test を用います。
$ npm install —save-dev start-server-and-test
package.jsonのscriptsに以下を追記します。
{
...
"scripts": {
"test": "jest",
"start-server": "amplify mock api",
"ci": "start-server-and-test start-server http://localhost:20002 (http://localhost:20002/) test“
},
...
}
これにより、$ npm run ci
実行時に以下の処理が行われます
- 第一引数の
start-server
、すなわちamplify mock api
コマンドの実行によりAmplify Mockingを開始 - 第二引数の
http://localhost:20002
を監視し、amplify-appsync-simulatorの起動を待機 - 第三引数の
test
すなわちjest
を実行 - テストの実行終了後、Amplify Mockingを停止
実際に試してみましょう。($ amplify mock api が停止していることをご確認ください)
$ npm run ci
ユニットテストが無事実行できたかと思います。(途中AppSync Simulatorのエラーが出力されますが、期待通りの振る舞いです)
Amplify Consoleのセットアップ
下ごしらえができたので、Amplify Console でテストを実行します。
ここではGitHubにこれまでのコードをpushする流れはSkipさせていただきます。
- AWSのマネージメントコンソールからAmplify Consoleを開きます
- 右上の New app > Host web app をクリックします
- GitHubを選択し、 Continueをクリックします
- リポジトリブランチの追加でpushしたブランチをrepoとブランチを選択します
- ここまで $ amplify pushを実行していないので、ビルド設定の追加ではCreate new environmentを選択し、env名には好きな名前(mainline など)を入力します。
- ビルド設定の項目で、テスト項目を追加します
- 「次へ」をクリックし、「保存してデプロイ」します
数分待つとビルドとテストがクリアされ、デプロイされたことを確認できます。
ビルド設定にGraphQL APIのユニットテストを足す
このままだと特にユニットテストもせずデプロイされてしまいます。テストの設定を追加してみましょう
アプリの設定 > ビルドの設定 > ビルド設定の追加で編集をクリック
以下のように書き換え、「保存」します
version: 1
backend:
phases:
# IMPORTANT - Please verify your build commands
preBuild:
commands:
- amazon-linux-extras enable corretto8
- yum install -y java-1.8.0-amazon-corretto java-1.8.0-amazon-corretto-devel
- npm ci
- amplify pull --appId ${APP_ID} --envName main -y
- # Amplify Mockingの実行に必要なAmplifyプロジェクトの情報をpull
- # ${APP_ID}はご自身のIDに置き換えてください。Amplify ConsoleでAppを開いて、#/の次の文字列です。(例: d3j54ikssyzl4d)
build:
commands:
- '# Execute Amplify CLI with the helper script'
- npm run ci && amplifyPush --simple #ユニットテストが通った時のみデプロイ
frontend:
phases:
preBuild:
commands:
- npm ci
build:
commands: []
artifacts:
# IMPORTANT - Please verify your build output directory
baseDirectory: /
files:
- '**/*'
cache:
paths:
- node_modules/**/*
(注1) Amplify Consoleのビルド環境はAmazon Linux2で、$ amplify mock api に必要なJavaランタイムが入っていません。そのため本記事ではAWSが提供する無償OpenJDK Distributionである Amazon Corretto をビルド時にインストールしています。ただしこれではCI/CDパイプラインが動くたびにJavaをインストールすることになり、ビルドの実行時間が伸びてしまいます。実際に利用するときはCustom build images機能を用いて、Javaをインストール済みのコンテナを利用することをおすすめします。
(注2) Add end-to-end tests to your appのように、test
セクションを用いる事も可能ですが、test
セクションに書いた内容はbackend
とfrontend
のセクションが実行された後に実行されます。その場合$ npm run ci
を用いたユニットテストが通ろうが通るまいが、バックエンドリソースが更新されてしまいます。そのため本記事では npm run ci && amplifyPush --simple
とすることで、ユニットテストが通らない場合にバックエンドリソースの更新をしないようにしています。
ビルドのページに戻って「このバージョンを再デプロイ」ボタンをクリックします。
数分待つとビルドとデプロイが実行されました!
ユニットテストが実行され、パスしてることが確認できます。
ユニットテストが通らなかった場合の挙動の確認
ユニットテストが通らなかった場合に、デプロイが実行されないことを確認しましょう
auth.test.js
に必ず失敗するテストを追加します
...
describe('Todo Model', () => {
//[追加部分]必ず失敗するテスト
test('must fail', () => {
expect(0).toStrictEqual(1);
})
//以下は同じ
test('Only owner can update their todos', async () => {
const testTodo = {
name: 'Test task',
description: 'This is a test task for unit test',
};
// Test用Todoの作成
const created = await clients[0].request(createTodo, {input: testTodo});
// Owner自身によるUpdateが成功することを確認
const updatedName = 'Updated Test Task by user_1';
const updatedByOwner = await clients[0].request(updateTodo, {input: {id: created.createTodo.id, name: updatedName}});
expect(updatedByOwner.updateTodo.name).toStrictEqual(updatedName);
// Owner以外によるUpdateが失敗することを確認
const updatedByOthers = clients[1].request(updateTodo, {input: {id: created.createTodo.id, name: ''}});
await expect(updatedByOthers).rejects.toThrowError('ConditionalCheckFailedException');
});
});
この内容をgitにpushし、再度デプロイの様子をみてみます。
確かにビルドフェーズで失敗しており、amplifyPush --simple
が実行されないことが確認できました!
Tips & etc
リクエストヘッダの生成
AmplifyのGraphQL APIで利用するAppSyncではCognito、IAM、API_KEY、OIDCの4つの認証方法が利用できます。
ここでは、IAM、API_KEY認証のリクエストヘッダの作り方をご紹介します。
(著者の環境ではAmplify MockingのOIDC認証がUnauthorizedException
になってしまい解決できず、、ここでは割愛させていただきます...><)
IAMの場合
リクエストヘッダに以下を付与します
const iam_key_client = new GraphQLClient('http://localhost:20002/graphql', {
headers: {
'Authorization': 'AWS4-HMAC-SH256 IAMAuthorized'
}
})
API_KEYの場合
const api_key_client = new GraphQLClient('http://localhost:20002/graphql', {
headers: {
'x-api-key': 'da2-fakeApiId123456'
}
})
Seed Dataの作成
残念ながらAmplify CLIのAmplify MockingではSeed Dataの作成をサポートしていません。
PFRが上がっているので清き+1をぜひ!
https://github.com/aws-amplify/amplify-cli/issues/2563#issuecomment-541873258
VTL単体のテスト
Amplify CLI を使っていると生VTLを書くことはほとんどありませんが、Custom VTL単体でtestしたい場合は、@G-awaさん著 Effective AppSync 〜 Serverless Framework を使用した AppSync の実践的な開発方法とテスト戦略 〜 の「VTLをテストする」の項目が非常に参考になります。
まとめ
Amplify Mockingを活用したGraphQL APIのユニットテストと、ユニットテストをAmplify Consoleでのデプロイ時に実行するところまでをご紹介しました。日々の開発にお役立ていただけましたら幸いです!!!