Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up for free
Effective AppSync ~ Serverless Framework を使用した AppSync の実践的な開発方法とテスト戦略 ~ / Effective AppSync
g-awa
December 02, 2020
Programming
0
880
Effective AppSync ~ Serverless Framework を使用した AppSync の実践的な開発方法とテスト戦略 ~ / Effective AppSync
g-awa
December 02, 2020
Tweet
Share
More Decks by g-awa
See All by g-awa
サーバレス時代の負荷テスト戦略 / Load testing strategy for serverless
gawa
18
4k
オレをCI/CDする / my ci cd practice
gawa
6
2.3k
AWSのテスト技法とPolicy as Code / aws-testing-techniques-and-policy-as-a-code
gawa
4
930
Other Decks in Programming
See All in Programming
23年のJavaトレンドは?Quarkusで理解するコンテナネイティブJava
tatsuya1bm
1
110
爆速の日経電子版開発の今
shinyaigeek
1
430
AWS App Runnerがそろそろ本番環境でも使い物になりそう
n1215
PRO
0
860
コンピュータビジョンセミナー2 / computer_vision_seminar_libSGM
fixstars
0
310
Quarto Tips for Academic Presentation
nicetak
0
890
新卒2年目がデータ分析API開発に挑戦【Stapy#88】/data-science-api-begginer
matsuik
0
330
eBPF와 함께 이해하는 Cilium 네트워킹
hadaney
3
830
量子コンピュータ時代のプログラミングセミナー / 20221222_Amplify_seminar _route_optimization
fixstars
0
240
jq at the Shortcuts
cockscomb
1
390
はてなリモートインターンシップ2022 フロントエンドブートキャンプ 講義資料
hatena
0
110
An Advanced Introduction to R
nicetak
0
1.7k
はてなリモートインターンシップ2022 Web API 講義資料
hatena
0
150
Featured
See All Featured
Documentation Writing (for coders)
carmenintech
51
2.9k
Fireside Chat
paigeccino
16
1.8k
Faster Mobile Websites
deanohume
295
29k
Stop Working from a Prison Cell
hatefulcrawdad
263
18k
Navigating Team Friction
lara
176
12k
The Cult of Friendly URLs
andyhume
68
5.1k
KATA
mclloyd
12
9.7k
Happy Clients
brianwarren
90
5.8k
What’s in a name? Adding method to the madness
productmarketing
12
1.9k
VelocityConf: Rendering Performance Case Studies
addyosmani
317
22k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
182
15k
Visualization
eitanlees
128
12k
Transcript
Effective AppSync ~ Serverless Framework を使⽤した AppSync の実践的な開発⽅法とテスト戦略 ~ 2020.12.02
Serverless MeetUp Japan Virtual #12
淡路⼤輔 / INTEC @gee0awa 好きな技術 Serverless / React Native /
Testing
AppSync の 始め⽅ AppSync と テスト What is GraphQL
AppSync の 始め⽅ AppSync と テスト What is GraphQL
What is GraphQL ? Facebookにより開発されたAPI ⽤のクエリ⾔語 Query / Mutation /
Subscription でデータ操作 Schema と呼ばれる型システム
REST vs GraphQL
API Document Mock Server Client SDK API Server の 型定義
OpenAPI による 恩恵 YAMLファイルにAPI定義を書き下すことで、各種ソースコードを⾃動⽣成
/users/:userId /users/:userId/friends friends users /posts Posts RESTful API の 課題
・1つの画⾯を表⽰するために幾つものAPIを実⾏しなければいけない ・バックエンドAPIによって、叩き⽅が異なる ・APIドキュメントに書いてあることと、実装がズレてしまう OpenAPI によって WebAPI は開発しやすくなってきたが・・・
user: { name: bob imageUrl: https://image.com/bob } posts: [ {
id: 1, imageUrl: https://image.com/1 title: xxxyyyzzz }, { id: 2, imageUrl: https://image.com/2 title: aabbcc }, … ] ブログサイト / ユーザプロフィール画⾯ ユーザの情報と投稿したブログの⼀覧を表⽰したい
/users/:userId user { name imageUrl age group } 1、ユーザを1件取得 /users/:userId/posts
items: [ { id title }, … ] 2、ユーザの記事を複数取得 /users/:userId/posts/:postId { id title imageUrl content } 3、記事の詳細をIDごとに取得 REST API Overfetching Underfetching and N+1 Problem 記事の数だけ何度もAPIを実⾏してしまう Client 不要なフィールドも取得してしまう RESTful API / Data Fetching
GraphQL API /graphql Client query { getUser(id: “1”) { name
imageUrl posts: { id title imageUrl } } } { name: “⽥中太郎” imageUrl: “http://xxx” posts: [ { id: “0001” title: “沖縄滞在記その1” imageUrl: “https://xxx" }, { id: “0002” title: “沖縄滞在記その2” imageUrl: “https://xxx" }, ] } GraphQL API / Data Fetching 必要なものだけを指定して取得できる Overfetching を解消 内部で Relation の解決を⾏う Underfetching や N+1 problem を解消
type Query { getUser(id: ID!): User } type User {
id: ID! name: String email: String imageUrl: String posts: [Post] } type Post { id: ID! title: String imageUrl: String content: String author: User } query { getUser(id: “1”) { id name posts: { id title imageUrl } } } { id: “1” name: “⽥中太郎” posts: [ { id: “0001” title: “沖縄滞在記その1” imageUrl: “https://xxx" }, { id: “0002” title: “沖縄滞在記その2” imageUrl: “https://xxx" }, ] } Schema Request Response SDL(Schema Definition Language) を使⽤して型を記述する 必要なフィールドを指定して リクエストする リクエストしたフィールドだけが 返却される Core Concepts of GraphQL
Core Concepts of GraphQL Query 取得 Mutation 変更 Subscription 購読
3種類 の データ操作 query { getUser(id: “1”) { name } } mutation { createUser(name: “John”, email: “
[email protected]
”) { name } } subscription { addedUser() { name } } mutation createUser { data: { name: “John”, email: “
[email protected]
" } } Mutation をトリガーにイベントベースでデータを購読する
Web API に 秩序 を 持たせる Query / Mutation /
Subscription でデータ操作 Schema と呼ばれる型システム フロントエンドの要件にあったデータを返却 Why GraphQL ?
GraphQL バックエンドを どのように組み⽴てるのか
GraphQL サーバ構築 の 苦労 VPC EC2 に GraphQL APIサーバ⼊れて「はい終わり」、ではない EC2
S VPC GraphQL サーバ構築 の 苦労 EC2 に GraphQL APIサーバ⼊れて「はい終わり」、ではない
Availability Zone Availability Zone Subscription するために 状態 を Redis に保持して Pub/Sub 冗⻑化、弾⼒性などを考慮して サーバにスケーラビリティを持たせる ELB ElasticCache for Redis CloudWatch Logs EC2のログを 出⼒する仕組みも必要 fluentd? awslogs? そもそもアプリケーション作るのも⾯倒 ・グローバルエラーハンドラー ・リゾルバーやルーティング処理 ・ビジネスロジック ・認証・認可の処理 ・etc・・・ EC2 EC2 GraphQL サーバは各種バックエンド データソースと接続する
AppSync AWS が提供するフルマネージド GraphQL サービス
AppSync を使⽤する 即座に⼿に⼊る GraphQL バックエンド CloudWatch Logs デフォルトでログを CloudWatchに出⼒ Cognito
Auth0 などの third party tool 認証・認可 各種サービスと統合 GraphQL エンドポイントを提供 内部のサーバは ⾃動的にスケールアウト GraphQL サーバは各種バックエンド データソースと接続する Lambda DynamoDB Elasticserach APIGateway Aurora Serverless
AppSync と テスト What is GraphQL AppSync の 始め⽅
AppSync を 構築する⽅法 AWS CLI マネジメントコンソール CloudFormation Amplify CLI Serverless
Framework 直感的なインターフェースで作成、簡単に検証できる 複数環境に全く同じ構成で構築しづらい。デリバリを⾃動化できない デリバリーを⾃動化できる 冪等性がないため、エラー時にリカバリが難しい 冪等性があり、エラー発⽣時に元に戻る スキーマ定義を S3 に配置しておく必要がある、YAML の量が多いなどの苦労 CloudFormation のコードを⾃動⽣成、抽象化されたコマンドで即座に構築可能 amplify コマンドが抽象化しすぎていて困るケースも・・・ 適度に抽象化しており、YAML の記述量は少なめ ローカルでテストできるツールセットが豊富 ⽤途に応じて選択する さくっと作るなら Amplify CLI、腰を据えてじっくり作るなら Serverless か CFn がオススメ 他にも AWS CDK や Pulumi, Terraform などもあります
AppSync を 構築する⽅法 AWS CLI マネジメントコンソール CloudFormation Amplify CLI Serverless
Framework 直感的なインターフェースで作成、簡単に検証できる 複数環境に全く同じ構成で構築しづらい。デリバリを⾃動化できない デリバリーを⾃動化できる 冪等性がないため、エラー時にリカバリが難しい 冪等性があり、エラー発⽣時に元に戻る スキーマ定義を S3 に配置しておく必要がある、YAML の量が多いなどの苦労 CloudFormation のコードを⾃動⽣成、抽象化されたコマンドで即座に構築可能 amplify コマンドが抽象化しすぎていて困るケースも・・・ 適度に抽象化しており、YAML の記述量は少なめ ローカルでテストできるツールセットが豊富 ⽤途に応じて選択する さくっと作るなら Amplify CLI、腰を据えてじっくり作るなら Serverless か CFn がオススメ 他にも AWS CDK や Pulumi, Terraform などもあります 今⽇はこれのお話しです
Serverless Framework for AppSync sid88in / serverless-appsync-plugin serverless-components / aws-app-sync
Serverless Framework の プラグイン ローカルに配置した schema.grapql や mapping-template を 参照して AppSync をデプロイする Serverless Components の AppSync コンポーネント 最⼩限のコード量で AppSync だけでなく、 カスタムエンドポイント付きで意味のある単位でリソース群をデプロイ https://github.com/sid88in/serverless-appsync-plugin https://github.com/serverless-components/aws-app-sync Serverless Framework と Serverless Components
Serverless Framework for AppSync Serverless Framework と Serverless Components sid88in
/ serverless-appsync-plugin serverless-components / aws-app-sync Serverless Framework の プラグイン ローカルに配置した schema.grapql や mapping-template を 参照して AppSync をデプロイする Serverless Components の AppSync コンポーネント 最⼩限のコード量で AppSync だけでなく、 カスタムエンドポイント付きで意味のある単位でリソース群をデプロイ https://github.com/sid88in/serverless-appsync-plugin https://github.com/serverless-components/aws-app-sync 今⽇はこれのお話しです
アプリ名 バージョン情報 タスク管理 テーブル サンプルアプリケーション タスク管理アプリケーションのバックエンドを AppSync で構築することを想定
アプリ名 バージョン情報 タスク管理 テーブル サンプルアプリケーション Schema, Resolver, DataSource の3つの要素 Query
{ getTask(id=“1”) { id name status } } ”data” { “getTask” { “id”: “1”, “name”: “掃除” “status”: “Done” } } AppSync Resolver (response) Resolver (request) Apache Velocity Template Language (VTL) で記述された マッピングテンプレートを使⽤して GraphQL リクエスト・レスポンスを データソースが理解できる形式に変換する レスポンスを変換、加⼯ リクエストを変換、加⼯ $util.toJson($ctx.result) Data Source { "version": "2018-05-29", "operation": "GetItem", "key": { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id) } }
Serverless AppSync Plugin serverless.yaml (続き) functions: appInfo: handler: src/functions/handler.appInfo name:
appInfo resources: Resources: Table: Type: AWS::DynamoDB::Table Properties: TableName: task AttributeDefinitions: - AttributeName: id AttributeType: S - AttributeName: status AttributeType: S KeySchema: - AttributeName: id KeyType: HASH - AttributeName: status KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 AppSyncDynamoDBServiceRole: Type: "AWS::IAM::Role" Properties: RoleName: appsync-dynamodb-role AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "appsync.amazonaws.com" Action: - "sts:AssumeRole" Policies: - PolicyName: "dynamo-policy" …ུ custom: appSync: name: taskboard_backend schema: schema.graphql dataSources: - type: AMAZON_DYNAMODB name: task description: λεΫཧςʔϒϧ config: tableName: { Ref: Table } serviceRoleArn: { Fn::GetAtt: [AppSyncDynamoDBServiceRole, Arn] } region: ap-northeast-1 - type: AWS_LAMBDA name: appInfo description: "Lambda DataSource for appInfo" config: functionName: appInfo iamRoleStatements: - Effect: "Allow" Action: - "lambda:invokeFunction" Resource: - “*" mappingTemplatesLocation: mapping-templates mappingTemplates: # ΞϓϦέʔγϣϯͷใΛऔಘ͢Δ - dataSource: appInfo type: Query field: appInfo request: Query.appInfo.request.vtl response: Query.appInfo.response.vtl # λεΫใΛ̍݅औಘ͢Δ - type: Query field: getTask request: “Query.getTask.request.vtl” response: “Query.getTask.response.vtl” plugin: - serverless-appsync-plugin serverless.yaml
Serverless AppSync Plugin serverless.yml (続き) functions: appInfo: handler: src/functions/handler.appInfo name:
appInfo resources: Resources: Table: Type: AWS::DynamoDB::Table Properties: TableName: task AttributeDefinitions: - AttributeName: id AttributeType: S - AttributeName: status AttributeType: S KeySchema: - AttributeName: id KeyType: HASH - AttributeName: status KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 AppSyncDynamoDBServiceRole: Type: "AWS::IAM::Role" Properties: RoleName: appsync-dynamodb-role AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "appsync.amazonaws.com" Action: - "sts:AssumeRole" Policies: - PolicyName: "dynamo-policy" …ུ custom: appSync: name: taskboard_backend schema: schema.graphql dataSources: - type: AMAZON_DYNAMODB name: task description: λεΫཧςʔϒϧ config: tableName: { Ref: Table } serviceRoleArn: { Fn::GetAtt: [AppSyncDynamoDBServiceRole, Arn] } region: ap-northeast-1 - type: AWS_LAMBDA name: appInfo description: "Lambda DataSource for appInfo" config: functionName: appInfo iamRoleStatements: - Effect: "Allow" Action: - "lambda:invokeFunction" Resource: - “*" mappingTemplatesLocation: mapping-templates mappingTemplates: # ΞϓϦέʔγϣϯͷใΛऔಘ͢Δ - dataSource: appInfo type: Query field: appInfo request: Query.appInfo.request.vtl response: Query.appInfo.response.vtl # λεΫใΛ̍݅औಘ͢Δ - type: Query field: getTask request: “Query.getTask.request.vtl” response: “Query.getTask.response.vtl” plugin: - serverless-appsync-plugin serverless.yml 使⽤するプラグインを宣⾔
Serverless AppSync Plugin serverless.yml (続き) functions: appInfo: handler: src/functions/handler.appInfo name:
appInfo resources: Resources: Table: Type: AWS::DynamoDB::Table Properties: TableName: task AttributeDefinitions: - AttributeName: id AttributeType: S - AttributeName: status AttributeType: S KeySchema: - AttributeName: id KeyType: HASH - AttributeName: status KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 AppSyncDynamoDBServiceRole: Type: "AWS::IAM::Role" Properties: RoleName: appsync-dynamodb-role AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "appsync.amazonaws.com" Action: - "sts:AssumeRole" Policies: - PolicyName: "dynamo-policy" …ུ custom: appSync: name: taskboard_backend schema: schema.graphql dataSources: - type: AMAZON_DYNAMODB name: task description: λεΫཧςʔϒϧ config: tableName: { Ref: Table } serviceRoleArn: { Fn::GetAtt: [AppSyncDynamoDBServiceRole, Arn] } region: ap-northeast-1 - type: AWS_LAMBDA name: appInfo description: "Lambda DataSource for appInfo" config: functionName: appInfo iamRoleStatements: - Effect: "Allow" Action: - "lambda:invokeFunction" Resource: - “*" mappingTemplatesLocation: mapping-templates mappingTemplates: # ΞϓϦέʔγϣϯͷใΛऔಘ͢Δ - dataSource: appInfo type: Query field: appInfo request: Query.appInfo.request.vtl response: Query.appInfo.response.vtl # λεΫใΛ̍݅औಘ͢Δ - type: Query field: getTask request: “Query.getTask.request.vtl” response: “Query.getTask.response.vtl” plugin: - serverless-appsync-plugin serverless.yml GraphQL のスキーマファイルを指定 データソースを指定 データソースとなるリソースも 同じテンプレートファイルで宣⾔して参照する
Serverless AppSync Plugin serverless.yml (続き) functions: appInfo: handler: src/functions/handler.appInfo name:
appInfo resources: Resources: Table: Type: AWS::DynamoDB::Table Properties: TableName: task AttributeDefinitions: - AttributeName: id AttributeType: S - AttributeName: status AttributeType: S KeySchema: - AttributeName: id KeyType: HASH - AttributeName: status KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 AppSyncDynamoDBServiceRole: Type: "AWS::IAM::Role" Properties: RoleName: appsync-dynamodb-role AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "appsync.amazonaws.com" Action: - "sts:AssumeRole" Policies: - PolicyName: "dynamo-policy" …ུ custom: appSync: name: taskboard_backend schema: schema.graphql dataSources: - type: AMAZON_DYNAMODB name: task description: λεΫཧςʔϒϧ config: tableName: { Ref: Table } serviceRoleArn: { Fn::GetAtt: [AppSyncDynamoDBServiceRole, Arn] } region: ap-northeast-1 - type: AWS_LAMBDA name: appInfo description: "Lambda DataSource for appInfo" config: functionName: appInfo iamRoleStatements: - Effect: "Allow" Action: - "lambda:invokeFunction" Resource: - “*" mappingTemplatesLocation: mapping-templates mappingTemplates: # ΞϓϦέʔγϣϯͷใΛऔಘ͢Δ - dataSource: appInfo type: Query field: appInfo request: Query.appInfo.request.vtl response: Query.appInfo.response.vtl # λεΫใΛ̍݅औಘ͢Δ - type: Query field: getTask request: “Query.getTask.request.vtl” response: “Query.getTask.response.vtl” plugin: - serverless-appsync-plugin serverless.yml マッピングテンプレートを宣⾔する ローカルに配置した VTL ファイル と クエリ を紐づける
Serverless AppSync Plugin $ serverless deploy serverless.yml query.vtl functions/handler.js デプロイ
CloudFormation Stack S3 Bucket AppSync Lambda Function DynamoDB Table ローカルの設定ファイルを読み込み、AWS上に各種リソースを構築します
AppSync の 始め⽅ AppSync と テスト What is GraphQL
AppSync をどのようにテストするのか? (今⽇話したいこと)
テスト の フィードバックループ https://speakerdeck.com/twada/testable-lambda-working-effectively-with-legacy-lambda?slide=19 Testable Lambda / t-wada先⽣ の 公演
@AWS Summit Tokyo 2017
AWS Cloud localhost AWS をエミュレートする テストコードによる 継続的な検証 ローカル環境に AWS の
Fake Object を作る ローカル環境に AWS と同じ環境を再現することで “テスタビリティをこじあける”
AWS サービスの Fake といえば LocalStack: ローカルに http通信できる AWS のモック(Fake)となるエンドポイントを起動する が有名ですが、
AppSync は 有料です
$ amplify mock api コマンドでローカルに AppSync のシミュレータを起動 amplify-appsync-simulator
serverless-appsync-simulator amplify-appsync-simulator をラップし、 ServerlessFramework のプラグインとして提供している
serverless-appsync-simulator plugin: - serverless-dynamodb-local - serverless-appsync-simulator - serverless-offline - serverless-appsync-plugin
← Dynamodb リゾルバーを使う場合は必要 ← sls offline として起動する $ sls offline start … AppSync Simulator: AppSync endpoint: http://localhost:20002/graphql AppSync Simulator: GraphiQl: http://localhost:20002 … serverless.yaml terminal serverless offline プラグインとして起動する ← serverless-offline より前に書くことに注意
serverless-appsync-simulator http://localhost:20002 に起動した GrapiQL
テストを書く準備ができました ☕
アプリ名 バージョン情報 タスク管理 テーブル Integration Testing AppSync の リゾルバー を
Black Box 的にテストを記述する Query { getTask(id=“1”) { id name status } } ”data” { “getTask” { “id”: “1”, “name”: “掃除” “status”: “Done” } } GraphQLのリクエストに対するレスポンスが 期待した通りかどうかテストします。 INPUT OUTPUT AppSync Data Source Resolver (response) Resolver (request) レスポンスを変換、加⼯ リクエストを変換、加⼯ $util.toJson($ctx.result) { "version": "2018-05-29", "operation": "GetItem", "key": { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id) } }
Integration Testing Jest (JavaScriptのテストライブラリ) によるコード例 import { GraphQLClient, gql }
from "graphql-request"; import { getTask } from "./query"; import { createTask, deleteTask } from "./mutation"; const client = new GraphQLClient("http://localhost:20002/graphql"); describe("Dynamodb resolver Integration Testing", () => { test("createTask / getTask by id / deleteTask", async () => { const request = { id: “123456789", name: "study serverless framework”, status: “NoStatus” }; // ̍݅λεΫσʔλΛ࡞ const created = await client.request(createTask, request); expect(created).toStrictEqual({ createTask: request }); // ࡞ͨ݁͠ՌΛऔಘ const got = await client.request(getTask, { id: request.id }); expect(got.getTask).toEqual(request); // ࡞ͨ͠λεΫσʔλΛআ const deleted = await client.request(deleteTask, { id: request.id }); expect(deleted).toStrictEqual({ deleteTask: request }); }); });
Integration Testing Jest (JavaScriptのテストライブラリ) によるコード例 import { GraphQLClient, gql }
from "graphql-request"; import { getTask } from "./query"; import { createTask, deleteTask } from "./mutation"; const client = new GraphQLClient("http://localhost:20002/graphql"); describe("Dynamodb resolver Integration Testing", () => { test("createTask / getTask by id / deleteTask", async () => { const request = { id: “123456789", name: "study serverless framework”, status: “NoStatus” }; // ̍݅λεΫσʔλΛ࡞ const created = await client.request(createTask, request); expect(created).toStrictEqual({ createTask: request }); // ࡞ͨ݁͠ՌΛऔಘ const got = await client.request(getTask, { id: request.id }); expect(got.getTask).toEqual(request); // ࡞ͨ͠λεΫσʔλΛআ const deleted = await client.request(deleteTask, { id: request.id }); expect(deleted).toStrictEqual({ deleteTask: request }); }); });
アプリ名 バージョン情報 タスク管理 テーブル Unit Testing ビジネスロジックが 凝集する リゾルバー をユニットテストする
Query { getTask(id=“1”) { id name status } } ”data” { “getTask” { “id”: “1”, “name”: “掃除” “status”: “Done” } } VTL で記述された mapping-template が リクエストに応じてどのような加⼯を⾏うかテストする OUTPUT INPUT AppSync Data Source Resolver (response) Resolver (request) レスポンスを変換、加⼯ リクエストを変換、加⼯ $util.toJson($ctx.result) { "version": "2018-05-29", "operation": "GetItem", "key": { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id) } }
Unit Testing amplify-velocity-template (Velocity.js) を使⽤して VTL ファイル を Compile /
Parse するヘルパー関数を⽤意する import * as fs from "fs"; import * as path from "path"; import { Compile, parse } from "amplify-velocity-template"; import { map } from "amplify-appsync-simulator/lib/velocity/value-mapper/mapper"; import * as utils from "amplify-appsync-simulator/lib/velocity/util"; // VTLϑΝΠϧͰల։͞ΕΔ context Λ࡞͢Δ const createVtlContext = <T>(args: T) => { const util = utils.create([], new Date(Date.now()), Object()); const context = { args, arguments: args, }; return { util, utils: util, ctx: context, context, }; }; // ࢦఆύεͷϑΝΠϧΛࢀর͠ɺೖྗύϥϝʔλΛͱʹɺvtlϑΝΠϧʹΑΓϚοϐϯά͞ΕͨϦκϧόϦΫΤετJSONΛϩʔυ͢Δ const vtlLoader = (filePath: string, args: any) => { const vtlPath = path.resolve(__dirname, filePath); const vtl = parse(fs.readFileSync(vtlPath, { encoding: "utf8" })); const compiler = new Compile(vtl, { valueMapper: map, escape: false }); const context = createVtlContext(args); const result = JSON.parse(compiler.render(context)); return result; }; ← VTLファイルをロード・パース・コンパイル
Unit Testing GraphQL リクエスト を リゾルバー が変換した結果を検査する test("getTask.req.vtl", () =>
{ const args = { id: "000" }; const result = vtlLoader(“./getTask.request.vtl", args); expect(result).toStrictEqual({ version: "2018-05-29", operation: "GetItem", key: { id: { S: "000" } } }); }); { "version": "2018-05-29", "operation": "GetItem", "key": { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id) } } getTask.request.vtl (VTLファイル) getTask.resolver.test.ts (テストコード) ← GraphQL のリクエストから Resolver に渡される引数の情報(INPUT) ← mapping-template によって⽣成された結果(OUTPUT) ← mapping-template の振る舞いを模したヘルパ関数
mutation.createTask.request.vtl (VTLファイル) Unit Testing 1件取得系の処理はシンプルだが、Mutation は if ⽂が増えて複雑になりがち ## [Start]
Prepare DynamoDB PutItem Request. ** $util.qr($context.args.input.put("createdAt", $util.defaultIfNull($ctx.args.input.createdAt, $util.time.nowISO8601()))) $util.qr($context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $util.time.nowISO8601()))) $util.qr($context.args.input.put("__typename", "Task")) #set( $condition = { "expression": "attribute_not_exists(#id)", "expressionNames": { "#id": "id" } } ) #if( $context.args.condition ) #set( $condition.expressionValues = {} ) #set( $conditionFilterExpressions = $util.parseJson($util.transform.toDynamoDBConditionExpression($context.args.condition)) ) $util.qr($condition.put("expression", "($condition.expression) AND $conditionFilterExpressions.expression")) $util.qr($condition.expressionNames.putAll($conditionFilterExpressions.expressionNames)) $util.qr($condition.expressionValues.putAll($conditionFilterExpressions.expressionValues)) #end #if( $condition.expressionValues && $condition.expressionValues.size() == 0 ) #set( $condition = { "expression": $condition.expression, "expressionNames": $condition.expressionNames } ) #end { "version": "2017-02-28", "operation": "PutItem", "key": { "id": $util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.args.input.id, $util.autoId())), "status": $util.dynamodb.toDynamoDBJson($context.args.input.status) }, "attributeValues": $util.dynamodb.toMapValuesJson($context.args.input), "condition": $util.toJson($condition) } ## [End] Prepare DynamoDB PutItem Request. **
test("createTask.req.vtl / expect attributeValues: createdAt, updateAt etc...", () => {
const args = { input: { id: “001", name: “study", status: “InProgress" } }; const result = vtlLoader(“./Mutation.createTask.request.vtl”, args); expect(result).toEqual({ version: "2017-02-28", operation: "PutItem", key: { id: { S: "001" }, status: { S: "InProgress" }, }, attributeValues: { __typename: { S: "Task", }, createdAt: { S: expect.anything(), }, id: { S: "001", }, name: { S: "study", }, status: { S: "InProgress", }, updatedAt: { S: expect.anything(), }, }, condition: { expression: "attribute_not_exists(#id)", expressionNames: { "#id": "id", }, }, }); }); Unit Testing テストケースはシンプルに保つことができる ✨ ← INPUT ← OUTPUT
CircleCI でテストを実⾏する
CircleCI でテストを実⾏する serverless-appsync-simulator が起動するまで テストの実⾏ を待たせる必要があるので、start-server-and-test を使⽤する version: 2.1 orbs:
aws-cli: circleci/
[email protected]
serverless: circleci/
[email protected]
jobs: test: executor: serverless/default steps: - checkout - run: name: apt update command: sudo apt update - run: name: apt install java command: sudo apt install openjdk-8-jdk - run: name: install dependencies command: yarn install - run: name: setup for dynamodb local command: yarn sls:setup - run: name: unit test command: yarn ci workflows: version: 2 build_and_test: jobs: - test "scripts": { "sls:setup": "sls dynamodb install", "start": "sls offline start", "test": "jest", "start-server": "yarn start", "ci": "start-server-and-test start-server http://localhost:20002 test" } .circleci/config.yml package.json serverless-appsync-simulator を起動する jest によるテストの実⾏ serverless-appsync-simulator を停⽌する
まとめ AppSync の 始め⽅ AppSync と テスト What is GraphQL
・GraphQL は Facebookにより開発されたAPI ⽤のクエリ⾔語 ・Query / Mutation / Subscription でデータ操作 ・Schema と呼ばれる型システム ・Schema, Resolver, DataSource の3つで構成 ・Serverless Framework か Amplify CLI がオススメ ・ローカルで AppSync のシミュレータを起動 ・GraphQL リクエスト - レスポンスをテストする Integration Testing ・VTLファイルによるマッピングテンプレートの振る舞いをテストする Unit Testing
Appendix サンプルソースを serverless/examples として上げています。 以下コマンドで簡単に⼿元で動かすことができます。 $ serverless create \ ̶template-url
https://github.com/daisuke-awaji/serverless-appsync-offline-typescript-template \ ̶path app