使用 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を選ぶこと以外は全てデフォルトの選択肢で回答します

完成了一个具有简单架构的待办事项模型。
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,类似于以下内容。

进行单元测试
安装 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时将执行以下操作
-
- 通过执行第一个参数start-server,即amplify mock api命令来启动Amplify Mocking。
-
- 监听第二个参数http://localhost:20002,等待amplify-appsync-simulator启动。
-
- 执行第三个参数test,即运行jest。
- 在测试结束后,停止Amplify Mocking。
我们来实际尝试一下。(请确认 $ amplify mock api 已停止)
$ npm run ci
我认为单元测试已经成功执行了。(虽然会输出AppSync Simulator的错误,但这是期望的行为)
在中国本土进行Amplify Console的设置。
由于准备工作已经完成,我们将在 Amplify Console 上执行测试。
在这里,我将跳过将之前的代码推送到 GitHub 的步骤。
-
- 从AWS管理控制台中打开Amplify控制台
-
- 点击右上角的”New app” > “Host web app”
-
- 选择GitHub,点击”Continue”
-
- 选择要添加的仓库和分支作为repo和分支
-
- 由于还没有运行过$ amplify push,因此在添加构建设置时选择”Create new environment”,并输入喜欢的环境名称(如mainline)
-
- 在构建设置中添加测试项目
- 点击”下一步”,然后点击”保存并部署”

您可以等待几分钟,以确认代码构建和测试已成功通过,并已部署。
在构建设置中添加GraphQL API的单元测试。
如果继续下去,将会没有经过单元测试就进行部署。让我们添加测试配置试一试。

写出以下内容,并点击”保存”按钮。
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来确保在单元测试未通过时不更新后端资源。

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

你可以运行单元测试并确认通过。
确认了无法通过单元测试的情况下的行为。
請確保當單元測試失敗時,不會執行部署的動作。請在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,并再次观察部署情况。

实施阶段明显失败,确认无法执行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