使用 Amplify Mocking 进行 GraphQL API 的单元测试

你好,我是小土豆!

本文是 AWS Amplify Advent Calendar 2020 的第九天的文章。
2020年是一个令人兴奋的一年,Amplify iOS、Android正式发布,Amplify JS支持SSR,Flutter进入开发者预览阶段,还有Amplify Admin UI的推出等等,Amplify有很多令人期待的更新!
这次我想总结一下关于编写GraphQL API单元测试的技巧!

希望的读者

    • AmplifyでGraphQL APIのテストを書きたい人

 

    テストが要件に入ってくる開発でもAmplify使いたい人

请理解我们不会解释有关Amplify基本用法的内容。如果您想亲自动手学习Amplify,请参考以下的Workshop。

    https://amplify-sns.workshop.aws/ja/

确认动作环境

    • @aws-amplify/cli v4.34.0

 

    本記事で作成したコード: https://github.com/jaga810/amplify-graphql-test

GraphQL API的测试是什么?

在使用Amplify CLI构建GraphQL API时,许多人会使用Amplify Mocking来测试API响应是否符合预期。如果您还没有使用过Amplify Mocking,请务必参考AWS Amplify Mocking的入门指南,它将大大提高您的GraphQL API开发速度。

我相信在开发过程中,也有需要持续进行单元测试的需求。如果进行单元测试,可以确保以前实现的部分不会因为新代码的更改而出现问题,并且可以提高开发速度和质量。

在使用Amplify CLI的GraphQL API进行单元测试时,应该如何实施?

本文介绍了使用JavaScript测试框架Jest,在Amplify Mocking创建的本地服务器上执行GraphQL操作并进行测试的策略,以进行单元测试。此外,还使用Amplify Console的CI/CD流水线来运行这些测试。

创建GraphQL API

放大 Amplify 项目的初始化

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

创建GraphQL API

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

完成了一个具有简单架构的待办事项模型。

type Todo @model {
  id: ID!
  name: String!
  description: String
}

为了实施认可机制,将该架构进行如下更改。

type Todo
  @model
  @auth(
    rules: [
      {allow: owner, ownerField: "owner", operations: [create, read, update, delete]},
    ]
  )
{
  id: ID!
  name: String!
  description: String
}

现在只有最初创建Todo的所有者才能执行读取/更新/删除操作。

确认动作

在单元测试中,可以使用Amplify Mocking在本地环境中测试GraphQL API的运行情况。Amplify Mocking内部使用amplify-appsync-simulator和DynamoDB Local来实现。

$ amplify mock api
#amplify mock apiで聞かれる項目はすべてデフォルトの選択肢で回答します
...
AppSync Mock endpoint is running at http://192.168.1.2:20002 (http://192.168.1.2:20002/)

在最后显示的终端节点上,amplify-appsync-simulator正在运行,并且当您访问时会显示Amplify GraphiQL Explorer,类似于以下内容。

image (14).png

进行单元测试

安装 Jest

首先创建一个package.json文件。

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

安装了用于测试的必要库,包括Jest。

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

另外,在项目的根目录下创建以下文件。

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

写@auth的测试

我们将编写一个单元测试来确认只有所有者可以根据@auth指定进行更新。

在考试中安装所需的库。

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

由於Amplify JavaScript的庫無法自由修改傳遞給AppSync的標頭,所以我們使用輕量級的graphql-request作為GraphQL客戶端。
crypto和base64url用於模擬生成Cognito的JWT令牌。

我们将在项目的根目录下创建以下文件。

import { GraphQLClient } from 'graphql-request';
import crypto from 'crypto';
import base64url from 'base64url';

import { createTodo, updateTodo } from './src/graphql/mutations';

const cognitoJwtGenerator = ({username}) => {
  const header = {
    'alg': 'HS256',
    'typ': 'JWT'
  }

  const payload = {
    'sub': '7d8ca528-4931-4254-9273-ea5ee853f271',
    'cognito:groups': [],
    'email_verified': true,
    'algorithm': 'HS256',
    'iss': 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_fake_idp',
    'phone_number_verified': true,
    'cognito:username': username,
    'cognito:roles': [],
    'aud': '2hifa096b3a24mvm3phskuaqi3',
    'event_id': '18f4067e-9985-4eae-9f33-f45f495470d0',
    'token_use': 'id',
    'phone_number': '+12062062016',
    'exp': 16073469193,
    'email': 'user@domain.com',
    'auth_time': 1586740073,
    'iat': 1586740073
  }

  const encodedHeaderPlusPayload = base64url(JSON.stringify(header)) + '.' + base64url(JSON.stringify(payload));

  const hmac = crypto.createHmac('sha256', 'secretKey')
  hmac.update(encodedHeaderPlusPayload)

  return encodedHeaderPlusPayload + '.' + hmac.digest('hex');
}

//2ユーザーからリクエストを行えるよう2つのクライアントを作成
const testUsers = ['user_0', 'user_1'];
const clients = [];

clients.push(new GraphQLClient('http://localhost:20002/graphql', {
  headers: {
    Authorization: cognitoJwtGenerator({username: testUsers[0]})
  },
}));

clients.push(new GraphQLClient('http://localhost:20002/graphql', {
  headers: {
    Authorization: cognitoJwtGenerator({username: testUsers[1]})
  },
}));

describe('Todo Model', () => {
  test('Only owner can update their todos', async () => {
    const testTodo = {
      name: 'Test task',
      description: 'This is a test task for unit test',
    };

    // Test用Todoの作成
    const created = await clients[0].request(createTodo, {input: testTodo});

    // Owner自身によるUpdateが成功することを確認
    const updatedName = 'Updated Test Task by user_1';
    const updatedByOwner = await clients[0].request(updateTodo, {input: {id: created.createTodo.id, name: updatedName}});
    expect(updatedByOwner.updateTodo.name).toStrictEqual(updatedName);

    // Owner以外によるUpdateが失敗することを確認
    const updatedByOthers =  clients[1].request(updateTodo, {input: {id: created.createTodo.id, name: ''}});
    await expect(updatedByOthers).rejects.toThrowError('ConditionalCheckFailedException');
  });
});

让我们运行测试吧(请确保 $ amplify mock api 正常工作)。

$ npm run test
graphql-test@1.0.0 test /Users/daisnaga/Dev/amplify-playground/graphql-test
> jest

PASS ./auth.test.js
Todo Model
✓ Only owner can update their todos (166 ms)

Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.65 s
Ran all test suites.

考试通过了!

使用CI/CD流水線(Amplify控制台)来执行GraphQL API的单元测试。

通过一个命令来启动AppSync模拟器并执行测试。

可以使用 bahmutov/start-server-and-test 模块实现一条命令即可完成从启动 AppSync 模拟器到执行测试的操作。这样做的好处是避免了在本地重复执行 AppSync 模拟器的启动过程,但在使用 Amplify Console 或其他 CI/CD 流水线时可能不太方便。

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

在package.json的scripts中添加以下内容。

{
  ...
  "scripts": {
    "test": "jest",
    "start-server": "amplify mock api",
    "ci": "start-server-and-test start-server http://localhost:20002 (http://localhost:20002/) test“
  },
  ...
}

这将使得在运行$ npm run ci时将执行以下操作

    1. 通过执行第一个参数start-server,即amplify mock api命令来启动Amplify Mocking。

 

    1. 监听第二个参数http://localhost:20002,等待amplify-appsync-simulator启动。

 

    1. 执行第三个参数test,即运行jest。

 

    在测试结束后,停止Amplify Mocking。

我们来实际尝试一下。(请确认 $ amplify mock api 已停止)

$ npm run ci

我认为单元测试已经成功执行了。(虽然会输出AppSync Simulator的错误,但这是期望的行为)

在中国本土进行Amplify Console的设置。

由于准备工作已经完成,我们将在 Amplify Console 上执行测试。
在这里,我将跳过将之前的代码推送到 GitHub 的步骤。

    1. 从AWS管理控制台中打开Amplify控制台

 

    1. 点击右上角的”New app” > “Host web app”

 

    1. 选择GitHub,点击”Continue”

 

    1. 选择要添加的仓库和分支作为repo和分支

 

    1. 由于还没有运行过$ amplify push,因此在添加构建设置时选择”Create new environment”,并输入喜欢的环境名称(如mainline)

 

    1. 在构建设置中添加测试项目

 

    点击”下一步”,然后点击”保存并部署”
image.png

您可以等待几分钟,以确认代码构建和测试已成功通过,并已部署。

在构建设置中添加GraphQL API的单元测试。

如果继续下去,将会没有经过单元测试就进行部署。让我们添加测试配置试一试。

Screen Shot 2020-12-09 at 9.11.48.png

写出以下内容,并点击”保存”按钮。

version: 1

backend:
  phases:
    # IMPORTANT - Please verify your build commands
    preBuild:
      commands:
        - amazon-linux-extras enable corretto8 
        - yum install -y java-1.8.0-amazon-corretto java-1.8.0-amazon-corretto-devel
        - npm ci
        - amplify pull --appId ${APP_ID} --envName main -y 
        - # Amplify Mockingの実行に必要なAmplifyプロジェクトの情報をpull
        - # ${APP_ID}はご自身のIDに置き換えてください。Amplify ConsoleでAppを開いて、#/の次の文字列です。(例: d3j54ikssyzl4d)
    build:
      commands:
        - '# Execute Amplify CLI with the helper script'
        - npm run ci && amplifyPush --simple #ユニットテストが通った時のみデプロイ
frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands: []
  artifacts:
    # IMPORTANT - Please verify your build output directory
    baseDirectory: /
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*

(注1)Amplify Console的构建环境为Amazon Linux 2,不包含在$ amplify mock api所需的Java运行时。因此,本文将在构建时安装由AWS提供的免费OpenJDK分发版Amazon Corretto。然而,这样每次CI/CD流水线运行时都需要安装Java,导致构建执行时间延长。实际使用时,建议使用Custom build images功能,利用已安装Java的容器。

类似于添加端到端测试到您的应用程序中,也可以使用test部分,但是在test部分的内容在backend和frontend部分执行之后才会执行。在这种情况下,使用$ npm run ci进行单元测试无论测试是否通过,都会导致后端资源被更新。因此,在本文中我们使用npm run ci && amplifyPush –simple来确保在单元测试未通过时不更新后端资源。

Screen Shot 2020-12-09 at 9.19.50.png

等待几分钟后,构建和部署已经执行完毕!

image.png

你可以运行单元测试并确认通过。

确认了无法通过单元测试的情况下的行为。

請確保當單元測試失敗時,不會執行部署的動作。請在auth.test.js中新增一個必然失敗的測試。

...

describe('Todo Model', () => {
  //[追加部分]必ず失敗するテスト
  test('must fail', () => {
    expect(0).toStrictEqual(1);
  })

  //以下は同じ
  test('Only owner can update their todos', async () => {
    const testTodo = {
      name: 'Test task',
      description: 'This is a test task for unit test',
    };

    // Test用Todoの作成
    const created = await clients[0].request(createTodo, {input: testTodo});

    // Owner自身によるUpdateが成功することを確認
    const updatedName = 'Updated Test Task by user_1';
    const updatedByOwner = await clients[0].request(updateTodo, {input: {id: created.createTodo.id, name: updatedName}});
    expect(updatedByOwner.updateTodo.name).toStrictEqual(updatedName);

    // Owner以外によるUpdateが失敗することを確認
    const updatedByOthers =  clients[1].request(updateTodo, {input: {id: created.createTodo.id, name: ''}});
    await expect(updatedByOthers).rejects.toThrowError('ConditionalCheckFailedException');
  });
});

将此内容推送到git,并再次观察部署情况。

image.png

实施阶段明显失败,确认无法执行amplifyPush –simple!

技巧等等

生成请求头

在Amplify的GraphQL API中使用AppSync,您可以使用Cognito、IAM、API_KEY和OIDC这四种身份验证方法。在这里,我们将介绍IAM和API_KEY身份验证的请求头创建方法。在作者的环境中,Amplify模拟的OIDC身份验证会导致UnauthorizedException错误,无法解决,所以在这里我们将不再详述…><

对于我来说

在请求头中添加以下内容

const iam_key_client = new GraphQLClient('http://localhost:20002/graphql', {
  headers: {
    'Authorization': 'AWS4-HMAC-SH256 IAMAuthorized'
  }
})

如果是API_KEY的情况下 shì API_KEY de xià)

const api_key_client = new GraphQLClient('http://localhost:20002/graphql', {
  headers: {
    'x-api-key': 'da2-fakeApiId123456'
  }
})

创建种子数据

很遗憾,Amplify CLI的Amplify Mocking不支持创建种子数据。
由于已经提到了PFR,请务必给予纯净的+1!
https://github.com/aws-amplify/amplify-cli/issues/2563#issuecomment-541873258

只需要一种方式的话,以下是对该句的汉语原生的释义:
VTL单元测试

使用 Amplify CLI 并没有太多机会编写原始的 VTL,但如果想要单独测试 Custom VTL,可以参考 @G-awa 撰写的《Effective AppSync 〜 Serverless Framework を使用した AppSync 的实际开发方法和测试策略〜》中关于”VTLをテストする”的部分,非常有参考价值。

总结

我們介紹了如何使用 Amplify Mocking 進行 GraphQL API 的單元測試,以及如何在 Amplify Console 上部署時執行單元測試。希望這能對您的日常開發有所幫助!

请查阅相关资料。

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

 

    • Jestで非同期関数が例外を投げることをテストする。

 

    • Getting Started · Jest

 

    • この頃流行りのJestを導入して軽快にJSをテストしよう – Qiita

 

    • graphql-request – npm

 

    • JSON Web Tokens – jwt.io

 

    • base64url – npm

 

    Crypto | Node.js v15.3.0 Documentation
bannerAds