Slide 1

Slide 1 text

Effective AppSync ~ Serverless Framework を使⽤した AppSync の実践的な開発⽅法とテスト戦略 ~ 2020.12.02 Serverless MeetUp Japan Virtual #12

Slide 2

Slide 2 text

淡路⼤輔 / INTEC @gee0awa 好きな技術 Serverless / React Native / Testing

Slide 3

Slide 3 text

AppSync の 始め⽅ AppSync と テスト What is GraphQL

Slide 4

Slide 4 text

AppSync の 始め⽅ AppSync と テスト What is GraphQL

Slide 5

Slide 5 text

What is GraphQL ? Facebookにより開発されたAPI ⽤のクエリ⾔語 Query / Mutation / Subscription でデータ操作 Schema と呼ばれる型システム

Slide 6

Slide 6 text

REST vs GraphQL

Slide 7

Slide 7 text

API Document Mock Server Client SDK API Server の 型定義 OpenAPI による 恩恵 YAMLファイルにAPI定義を書き下すことで、各種ソースコードを⾃動⽣成

Slide 8

Slide 8 text

/users/:userId /users/:userId/friends friends users /posts Posts RESTful API の 課題 ・1つの画⾯を表⽰するために幾つものAPIを実⾏しなければいけない ・バックエンドAPIによって、叩き⽅が異なる ・APIドキュメントに書いてあることと、実装がズレてしまう OpenAPI によって WebAPI は開発しやすくなってきたが・・・

Slide 9

Slide 9 text

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 }, … ] ブログサイト / ユーザプロフィール画⾯ ユーザの情報と投稿したブログの⼀覧を表⽰したい

Slide 10

Slide 10 text

/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

Slide 11

Slide 11 text

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 を解消

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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 をトリガーにイベントベースでデータを購読する

Slide 14

Slide 14 text

Web API に 秩序 を 持たせる Query / Mutation / Subscription でデータ操作 Schema と呼ばれる型システム フロントエンドの要件にあったデータを返却 Why GraphQL ?

Slide 15

Slide 15 text

GraphQL バックエンドを どのように組み⽴てるのか

Slide 16

Slide 16 text

GraphQL サーバ構築 の 苦労 VPC EC2 に GraphQL APIサーバ⼊れて「はい終わり」、ではない EC2

Slide 17

Slide 17 text

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 サーバは各種バックエンド データソースと接続する

Slide 18

Slide 18 text

AppSync AWS が提供するフルマネージド GraphQL サービス

Slide 19

Slide 19 text

AppSync を使⽤する 即座に⼿に⼊る GraphQL バックエンド CloudWatch Logs デフォルトでログを CloudWatchに出⼒ Cognito Auth0 などの third party tool 認証・認可 各種サービスと統合 GraphQL エンドポイントを提供 内部のサーバは ⾃動的にスケールアウト GraphQL サーバは各種バックエンド データソースと接続する Lambda DynamoDB Elasticserach APIGateway Aurora Serverless

Slide 20

Slide 20 text

AppSync と テスト What is GraphQL AppSync の 始め⽅

Slide 21

Slide 21 text

AppSync を 構築する⽅法 AWS CLI マネジメントコンソール CloudFormation Amplify CLI Serverless Framework 直感的なインターフェースで作成、簡単に検証できる 複数環境に全く同じ構成で構築しづらい。デリバリを⾃動化できない デリバリーを⾃動化できる 冪等性がないため、エラー時にリカバリが難しい 冪等性があり、エラー発⽣時に元に戻る スキーマ定義を S3 に配置しておく必要がある、YAML の量が多いなどの苦労 CloudFormation のコードを⾃動⽣成、抽象化されたコマンドで即座に構築可能 amplify コマンドが抽象化しすぎていて困るケースも・・・ 適度に抽象化しており、YAML の記述量は少なめ ローカルでテストできるツールセットが豊富 ⽤途に応じて選択する さくっと作るなら Amplify CLI、腰を据えてじっくり作るなら Serverless か CFn がオススメ 他にも AWS CDK や Pulumi, Terraform などもあります

Slide 22

Slide 22 text

AppSync を 構築する⽅法 AWS CLI マネジメントコンソール CloudFormation Amplify CLI Serverless Framework 直感的なインターフェースで作成、簡単に検証できる 複数環境に全く同じ構成で構築しづらい。デリバリを⾃動化できない デリバリーを⾃動化できる 冪等性がないため、エラー時にリカバリが難しい 冪等性があり、エラー発⽣時に元に戻る スキーマ定義を S3 に配置しておく必要がある、YAML の量が多いなどの苦労 CloudFormation のコードを⾃動⽣成、抽象化されたコマンドで即座に構築可能 amplify コマンドが抽象化しすぎていて困るケースも・・・ 適度に抽象化しており、YAML の記述量は少なめ ローカルでテストできるツールセットが豊富 ⽤途に応じて選択する さくっと作るなら Amplify CLI、腰を据えてじっくり作るなら Serverless か CFn がオススメ 他にも AWS CDK や Pulumi, Terraform などもあります 今⽇はこれのお話しです

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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 今⽇はこれのお話しです

Slide 25

Slide 25 text

アプリ名 バージョン情報 タスク管理 テーブル サンプルアプリケーション タスク管理アプリケーションのバックエンドを AppSync で構築することを想定

Slide 26

Slide 26 text

アプリ名 バージョン情報 タスク管理 テーブル サンプルアプリケーション 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) } }

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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 使⽤するプラグインを宣⾔

Slide 29

Slide 29 text

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 のスキーマファイルを指定 データソースを指定 データソースとなるリソースも 同じテンプレートファイルで宣⾔して参照する

Slide 30

Slide 30 text

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 ファイル と クエリ を紐づける

Slide 31

Slide 31 text

Serverless AppSync Plugin $ serverless deploy serverless.yml query.vtl functions/handler.js デプロイ CloudFormation Stack S3 Bucket AppSync Lambda Function DynamoDB
 Table ローカルの設定ファイルを読み込み、AWS上に各種リソースを構築します

Slide 32

Slide 32 text

AppSync の 始め⽅ AppSync と テスト What is GraphQL

Slide 33

Slide 33 text

AppSync をどのようにテストするのか? (今⽇話したいこと)

Slide 34

Slide 34 text

テスト の フィードバックループ https://speakerdeck.com/twada/testable-lambda-working-effectively-with-legacy-lambda?slide=19 Testable Lambda / t-wada先⽣ の 公演 @AWS Summit Tokyo 2017

Slide 35

Slide 35 text

AWS Cloud localhost AWS をエミュレートする テストコードによる 継続的な検証 ローカル環境に AWS の Fake Object を作る ローカル環境に AWS と同じ環境を再現することで “テスタビリティをこじあける”

Slide 36

Slide 36 text

AWS サービスの Fake といえば LocalStack: ローカルに http通信できる AWS のモック(Fake)となるエンドポイントを起動する が有名ですが、 AppSync は 有料です

Slide 37

Slide 37 text

$ amplify mock api コマンドでローカルに AppSync のシミュレータを起動 amplify-appsync-simulator

Slide 38

Slide 38 text

serverless-appsync-simulator amplify-appsync-simulator をラップし、 ServerlessFramework のプラグインとして提供している

Slide 39

Slide 39 text

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 より前に書くことに注意

Slide 40

Slide 40 text

serverless-appsync-simulator http://localhost:20002 に起動した GrapiQL

Slide 41

Slide 41 text

テストを書く準備ができました ☕

Slide 42

Slide 42 text

アプリ名 バージョン情報 タスク管理 テーブル 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) } }

Slide 43

Slide 43 text

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 }); }); });

Slide 44

Slide 44 text

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 }); }); });

Slide 45

Slide 45 text

アプリ名 バージョン情報 タスク管理 テーブル 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) } }

Slide 46

Slide 46 text

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 = (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ファイルをロード・パース・コンパイル

Slide 47

Slide 47 text

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 の振る舞いを模したヘルパ関数

Slide 48

Slide 48 text

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. **

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

CircleCI でテストを実⾏する

Slide 51

Slide 51 text

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 を停⽌する

Slide 52

Slide 52 text

まとめ 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

Slide 53

Slide 53 text

Appendix サンプルソースを serverless/examples として上げています。 以下コマンドで簡単に⼿元で動かすことができます。 $ serverless create \ ̶template-url https://github.com/daisuke-awaji/serverless-appsync-offline-typescript-template \ ̶path app