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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  6. REST vs GraphQL

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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
    },

    ]
    ブログサイト / ユーザプロフィール画⾯
    ユーザの情報と投稿したブログの⼀覧を表⽰したい

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  31. Serverless AppSync Plugin
    $ serverless deploy
    serverless.yml
    query.vtl
    functions/handler.js
    デプロイ
    CloudFormation
    Stack
    S3
    Bucket
    AppSync
    Lambda
    Function
    DynamoDB

    Table
    ローカルの設定ファイルを読み込み、AWS上に各種リソースを構築します

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  50. CircleCI でテストを実⾏する

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide