Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Effective AppSync ~ Serverless Framework を使用した AppSync の実践的な開発方法とテスト戦略 ~ / Effective AppSync

g-awa
December 02, 2020

Effective AppSync ~ Serverless Framework を使用した AppSync の実践的な開発方法とテスト戦略 ~ / Effective AppSync

g-awa

December 02, 2020
Tweet

More Decks by g-awa

Other Decks in Programming

Transcript

  1. Effective AppSync ~ Serverless Framework を使⽤した AppSync の実践的な開発⽅法とテスト戦略 ~ 2020.12.02

    Serverless MeetUp Japan Virtual #12
  2. 淡路⼤輔 / INTEC @gee0awa 好きな技術 Serverless / React Native /

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

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

  5. What is GraphQL ? Facebookにより開発されたAPI ⽤のクエリ⾔語 Query / Mutation /

    Subscription でデータ操作 Schema と呼ばれる型システム
  6. REST vs GraphQL

  7. API Document Mock Server Client SDK API Server の 型定義

    OpenAPI による 恩恵 YAMLファイルにAPI定義を書き下すことで、各種ソースコードを⾃動⽣成
  8. /users/:userId /users/:userId/friends friends users /posts Posts RESTful API の 課題

    ・1つの画⾯を表⽰するために幾つものAPIを実⾏しなければいけない ・バックエンドAPIによって、叩き⽅が異なる ・APIドキュメントに書いてあることと、実装がズレてしまう OpenAPI によって WebAPI は開発しやすくなってきたが・・・
  9. 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 }, … ] ブログサイト / ユーザプロフィール画⾯ ユーザの情報と投稿したブログの⼀覧を表⽰したい
  10. /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
  11. 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 を解消
  12. 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
  13. 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 をトリガーにイベントベースでデータを購読する
  14. Web API に 秩序 を 持たせる Query / Mutation /

    Subscription でデータ操作 Schema と呼ばれる型システム フロントエンドの要件にあったデータを返却 Why GraphQL ?
  15. GraphQL バックエンドを どのように組み⽴てるのか

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

  17. 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 サーバは各種バックエンド データソースと接続する
  18. AppSync AWS が提供するフルマネージド GraphQL サービス

  19. AppSync を使⽤する 即座に⼿に⼊る GraphQL バックエンド CloudWatch Logs デフォルトでログを CloudWatchに出⼒ Cognito

    Auth0 などの third party tool 認証・認可 各種サービスと統合 GraphQL エンドポイントを提供 内部のサーバは ⾃動的にスケールアウト GraphQL サーバは各種バックエンド データソースと接続する Lambda DynamoDB Elasticserach APIGateway Aurora Serverless
  20. AppSync と テスト What is GraphQL AppSync の 始め⽅

  21. AppSync を 構築する⽅法 AWS CLI マネジメントコンソール CloudFormation Amplify CLI Serverless

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

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

  26. アプリ名 バージョン情報 タスク管理 テーブル サンプルアプリケーション 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) } }
  27. 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
  28. 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 使⽤するプラグインを宣⾔
  29. 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 のスキーマファイルを指定 データソースを指定 データソースとなるリソースも 同じテンプレートファイルで宣⾔して参照する
  30. 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 ファイル と クエリ を紐づける
  31. Serverless AppSync Plugin $ serverless deploy serverless.yml query.vtl functions/handler.js デプロイ

    CloudFormation Stack S3 Bucket AppSync Lambda Function DynamoDB
 Table ローカルの設定ファイルを読み込み、AWS上に各種リソースを構築します
  32. AppSync の 始め⽅ AppSync と テスト What is GraphQL

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

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

    @AWS Summit Tokyo 2017
  35. AWS Cloud localhost AWS をエミュレートする テストコードによる 継続的な検証 ローカル環境に AWS の

    Fake Object を作る ローカル環境に AWS と同じ環境を再現することで “テスタビリティをこじあける”
  36. AWS サービスの Fake といえば LocalStack: ローカルに http通信できる AWS のモック(Fake)となるエンドポイントを起動する が有名ですが、

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

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

  39. 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 より前に書くことに注意
  40. serverless-appsync-simulator http://localhost:20002 に起動した GrapiQL

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

  42. アプリ名 バージョン情報 タスク管理 テーブル 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) } }
  43. 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 }); }); });
  44. 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 }); }); });
  45. アプリ名 バージョン情報 タスク管理 テーブル 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) } }
  46. 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ファイルをロード・パース・コンパイル
  47. 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 の振る舞いを模したヘルパ関数
  48. 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. **
  49. 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
  50. CircleCI でテストを実⾏する

  51. 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 を停⽌する
  52. まとめ 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
  53. Appendix サンプルソースを serverless/examples として上げています。 以下コマンドで簡単に⼿元で動かすことができます。 $ serverless create \ ̶template-url

    https://github.com/daisuke-awaji/serverless-appsync-offline-typescript-template \ ̶path app