38
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Amplify Mockingを利用してGraphQL API のユニットテストをする

Last updated at Posted at 2020-12-09

どうも、じゃがです!

本記事は 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もご参照ください

動作確認環境

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 プロジェクトの初期化

shell
#適当な作業ディレクトリで以下のコマンドを実行します
$ mkdir graphql-test; cd $_
$ amplify init
#amplify initで聞かれる項目は全てデフォルトの選択肢で大丈夫です

GraphQL API の作成

shell
$ amplify add api
#amplify add apiで聞かれる項目は、認証方法でCognitoを選ぶこと以外は全てデフォルトの選択肢で回答します

image (13).png

シンプルなスキーマを持つ Todo モデルが出来上がりました

amplify/backend/api/graphqltest/schema.graphql
type Todo @model {
  id: ID!
  name: String!
  description: String
}

認可の仕組みを実装するために、このスキーマを以下のように書き換えます

amplify/backend/api/graphqltest/schema.graphql
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を利用しています

shell
$ 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が表示されます

image (14).png

ユニットテストの実行

Jestの導入

まずはpackage.jsonを作成します

shell
$ npm init
#test commandのみ jest と入力しましょう。$ npm testコマンドでjestが走るようになります

Jestをはじめとしたテストに必要なライブラリをインストールします

shell
$ npm install —save-dev jest babel-jest babel-plugin-transform-es2015-modules-commonjs

また、プロジェクト直下に以下のファイルを作成します

.babelrc
{
  "env": {
      "test": {
          "plugins": [
              "transform-es2015-modules-commonjs"
          ]
      }
  }
}
jest.config.js
module.exports = {
  transform: {
      '^.+\\.js$'  : '<rootDir>/node_modules/babel-jest',
  },
  moduleFileExtensions: ['js']
}

@authのテストを書く

@authで指定した通りに、ownerしかupdateができないことを確認するユニットテストを書いていきます

テスト内で必要なライブラリをインストールします

shell
npm install —save-dev graphql-request graphql crypto base64url

Amplify JavaScriptのライブラリはAppSyncに渡すヘッダを自由に変更できないので、軽量なgraphql-requestをGraphQLクライアントとして利用します
cryptoとbase64urlはCognitoのJWTトークンを擬似的に生成するために利用します

プロジェクト直下に以下のファイルを作成します

auth.test.js
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 が動いていることを確認してください)

shell
$ 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 を用います。

shell
$ npm install —save-dev start-server-and-test

package.jsonのscriptsに以下を追記します。

package.json
{
  ...
  "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実行時に以下の処理が行われます

  1. 第一引数のstart-server、すなわちamplify mock apiコマンドの実行によりAmplify Mockingを開始
  2. 第二引数のhttp://localhost:20002を監視し、amplify-appsync-simulatorの起動を待機
  3. 第三引数の test すなわち jest を実行
  4. テストの実行終了後、Amplify Mockingを停止

実際に試してみましょう。($ amplify mock api が停止していることをご確認ください)

$ npm run ci

ユニットテストが無事実行できたかと思います。(途中AppSync Simulatorのエラーが出力されますが、期待通りの振る舞いです)

Amplify Consoleのセットアップ

下ごしらえができたので、Amplify Console でテストを実行します。
ここではGitHubにこれまでのコードをpushする流れはSkipさせていただきます。

  1. AWSのマネージメントコンソールからAmplify Consoleを開きます
  2. 右上の New app > Host web app をクリックします
  3. GitHubを選択し、 Continueをクリックします
  4. リポジトリブランチの追加でpushしたブランチをrepoとブランチを選択します
  5. ここまで $ amplify pushを実行していないので、ビルド設定の追加ではCreate new environmentを選択し、env名には好きな名前(mainline など)を入力します。
  6. ビルド設定の項目で、テスト項目を追加します
  7. 「次へ」をクリックし、「保存してデプロイ」します

image.png

数分待つとビルドとテストがクリアされ、デプロイされたことを確認できます。

ビルド設定にGraphQL APIのユニットテストを足す

このままだと特にユニットテストもせずデプロイされてしまいます。テストの設定を追加してみましょう

アプリの設定 > ビルドの設定 > ビルド設定の追加で編集をクリック
Screen Shot 2020-12-09 at 9.11.48.png

以下のように書き換え、「保存」します

amplify.yml
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セクションに書いた内容はbackendfrontendのセクションが実行された後に実行されます。その場合$ npm run ciを用いたユニットテストが通ろうが通るまいが、バックエンドリソースが更新されてしまいます。そのため本記事では npm run ci && amplifyPush --simpleとすることで、ユニットテストが通らない場合にバックエンドリソースの更新をしないようにしています。

ビルドのページに戻って「このバージョンを再デプロイ」ボタンをクリックします。
Screen Shot 2020-12-09 at 9.19.50.png

数分待つとビルドとデプロイが実行されました!

image.png

ユニットテストが実行され、パスしてることが確認できます。

ユニットテストが通らなかった場合の挙動の確認

ユニットテストが通らなかった場合に、デプロイが実行されないことを確認しましょう
auth.test.jsに必ず失敗するテストを追加します

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し、再度デプロイの様子をみてみます。

image.png
image.png

確かにビルドフェーズで失敗しており、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でのデプロイ時に実行するところまでをご紹介しました。日々の開発にお役立ていただけましたら幸いです!!!

参考資料

38
20
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
38
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?