diff --git a/Gemfile.lock b/Gemfile.lock index 216b00d..53e5c68 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -29,6 +29,8 @@ GEM safe_yaml (~> 1.0) jekyll-sass-converter (1.5.2) sass (~> 3.4) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) kramdown (1.17.0) @@ -56,8 +58,9 @@ PLATFORMS DEPENDENCIES jekyll (~> 3.8) + jekyll-sitemap kramdown rouge BUNDLED WITH - 1.15.4 + 1.15.4 diff --git a/_config.yml b/_config.yml index 2832230..07bd9ee 100644 --- a/_config.yml +++ b/_config.yml @@ -30,4 +30,4 @@ kramdown: plugins: - jekyll-sitemap -version: 10 +version: 12 diff --git a/src/_layouts/book-landing.html b/src/_layouts/book-landing.html index b144eb8..f0608d9 100644 --- a/src/_layouts/book-landing.html +++ b/src/_layouts/book-landing.html @@ -16,7 +16,7 @@
-

{{ page.seriesTitle }}

+

{{ page.seriesTitle }}

{{ content }} diff --git a/src/_layouts/post.html b/src/_layouts/post.html index 46d9268..7578e03 100644 --- a/src/_layouts/post.html +++ b/src/_layouts/post.html @@ -18,11 +18,12 @@

{{ page.title }}

- {{ page.author_name }} in {{ page.categories[0] }} {% include read_time.html %} + {{ page.author_name }} in {{ page.categories[0] }} {% include read_time.html %} Last updated {{ page.date | date: "%b %-d, %Y" }}

- + {{ content }} + {% if page.show_related_posts == true %} + {% endif %}
diff --git a/src/_posts/2020-11-12-the-power-of-serverless-graphql-with-appsync.markdown b/src/_posts/2020-11-12-the-power-of-serverless-graphql-with-appsync.markdown index 8e903be..6f5fc89 100644 --- a/src/_posts/2020-11-12-the-power-of-serverless-graphql-with-appsync.markdown +++ b/src/_posts/2020-11-12-the-power-of-serverless-graphql-with-appsync.markdown @@ -1,7 +1,7 @@ --- layout: post title: "The Power of Serverless GraphQL with AWS AppSync" -excerpt: "A serverless application for handling webhooks using EventBridge event bus, API Gateway's HTTP API and Lambda function" +excerpt: "A story about serverless GraphQL on AWS with AWS AppSync" date: 2020-11-12 11:00:00 +0200 categories: Serverless author_name : Slobodan Stojanović diff --git a/src/_posts/2021-05-24-migrating-to-aws-sdk-v3.md b/src/_posts/2021-05-24-migrating-to-aws-sdk-v3.md new file mode 100644 index 0000000..54cc449 --- /dev/null +++ b/src/_posts/2021-05-24-migrating-to-aws-sdk-v3.md @@ -0,0 +1,191 @@ +--- +layout: post +title: "Migrating to AWS SDK v3 for Javascript" +excerpt: "tips, gotchas and surprises with the new AWS SDK" +date: 2021-05-27 01:00:00 +0000 +categories: + - Serverless +author_name : Gojko Adzic +author_url : /author/gojko +author_avatar: gojko.jpg +twitter_username: gojkoadzic +show_avatar: true +feature_image: change-seo.jpg +show_related_posts: false +square_related: recommend-gojko +--- + +AWS SDK for JavaScript is going through a major update, with version 3 becoming ready for production usage. The API is not backwards compatible with the old version, so migrating requires a significant change in client code. Some of it is for the better, some not so much. We've recently migrated a large project from v2 to v3. In this article, I'll go through the key points for migration, including the things that surprised us, including the stuff that required quite a lot of digging. + +## Why v3? + +The key advantage of v3 over v2 is modular design. Instead of a huge package that contains clients and metadata for all AWS services, with V3 you can include just the stuff you really need, which leads to smaller deployment bundles. This also means slightly faster startup times on AWS Lambda, for example, and faster page initialisation for client-side code. + +For people working with TypeScript, V3 is also designed ground-up to support type definitions. + +The new SDK is also written with Promises in mind, so there's no need to attach the ugly `.promise()` call after all API commands. + +Finally, the new SDK supports flexible middleware, so there's no need to create ugly wrappers and interceptors to modify how the SDK calls AWS APIs. For example, I had to write some horrible code to add retries to all API Gateway methods when building `claudia.js`. This can be done nicely with middleware now. + +## Key differences between V3 and V2 + +Instead of service objects that contain meaningful methods to access the API (for example, `s3.headObject` from the v2 SDK), in v3 each API endpoint maps to a `Command` object (for example `HeadObjectCommand`). The parameters and return types of the old and the new methods are mostly the same, so the execution code requires minimal or no changes (as long as you're using the low-level API commands, we'll come back to this later). Each service has a `Client` class, with a `send` method that accepts a command object. + +For example, the following two snippets produce the same results. The first is written with v2 SDK, the second with v3: + +```js +// v2 +const aws = require('aws-sdk'), + s3 = new aws.S3(), + result = await s3.headObject({ + Bucket: 'some-bucket', + Key: 'file-key', + VersionId: 'file-version' + }).promise(); + +// v3 +const { S3Client, HeadObjectCommand } = require('@aws-sdk/client-s3'), + result = await s3Client.send(new HeadObjectCommand({ + Bucket: 'some-bucket', + Key: 'file-key', + VersionId: 'file-version' + })); +``` + +Notice that there's no `.promise()` in v3 calls, and that the parameters are pretty much the same. The result structure is the same as well, so this code can just be swapped. + +Also note that the v3 code requires the (minimal) client and a specific command, so JavaScript bundlers produce much smaller results. Here are the results for the two snippets above: + +| variant | esbuild | esbuild --minify | +| --- | --- | --- | +| v2 | 13 MB | 5.4 MB | +| v3 | 1.4 MB | 666.9 KB | + +## Commands map to API directly + +The basic V3 SDK maps pretty much directly to the AWS service APIs, which means that the SDK clients are mostly automatically generated from AWS service definitions, including the documentation. The v2 documentation is amazing, with lots of examples to demonstrate how to use the key methods. V3 documentation is by comparison very basic. It's effectively a type reference, no more and no less. Hopefully as the SDK matures, someone at AWS will end up writing better docs. + +Although this looks as if it would be possible to just migrate between the SDK versions with a few lines of clever `sed` scripts, things get a bit more tricky with higher-level functions. The V2 SDK was closely related to the AWS service APIs, but not restricted by it. It also included a bunch of functions that made life much easier for JavaScript developers than if they used the bare-bones API directly. For example, the `S3` service object had a useful method for multipart batch uploading large files (`.upload()`) that doesn't exist in the API. Those methods do not exist as commands in the v3 SDK. Some utilities are provided in additional packages. This has the benefit of reducing bundle size for projects that do not need them, but it also means they are not as easy to discover as before. Here are some of the most important ones: + +* Utility methods for converting between DynamoDB structures and JavaScript types (`aws.DynamoDB.Converter`) are no longer in the basic `DynamoDB` service, but in the [`@aws-sdk/util-dynamodb`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_util_dynamodb.html) +* DynamoDB `DocumentClient` is now in [`@aws-sdk/lib-dynamodb`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_lib_dynamodb.html) +* The implementation for `s3.upload` is now in [`@aws-sdk/lib-storage`](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/README.md) +* The implementation for `s3.createPresignedPost` is now in [`@aws-sdk/s3-presigned-post`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_s3_presigned_post.html) +* The implementation for `s3.getSignedUrl` is now in [`@aws-sdk/s3-request-presigner`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/classes/_aws_sdk_s3_request_presigner.s3requestpresigner-1.html) + +The last one is an example where things get a bit tricky. The method changes from synchronous to asynchronous (so you'll have to update all the callers to `await` or return a `Promise`), and the expiry argument is no longer directly in the parameters - you need to pass it as a separate option to the signer. + +```js +//v2 +const s3 = aws = require('aws-sdk'), + s3 = new aws.S3(), + result = s3.getSignedUrl('getObject', { + Bucket: 'some-bucket', Key: 'file-key', Expires: expiresIn + }); + +//v3 +const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3'), + { getSignedUrl } = require('@aws-sdk/s3-request-presigner'), + command = new GetObjectCommand({Bucket: 'some-bucket', Key: 'file-key'}), + result = await getSignedUrl(s3Client, command, {expiresIn}); +``` + +Another place where good JS affordance utilities have been lost is retrieving the body of S3 objects. In v2, the `getObject` method had several utilities for consuming the result body into a string, or a buffer, or a stream. With v3 SDK, the result is a stream, and you'll have to convert it to string yourself. Here is a comparison of v2 and v3 code: + +```js +//v2 +const data = s3.getObject(params).promise(), + return data.Body.toString(); + +// v3 +const streamToString = function (stream) { + const chunks = []; + return new Promise((resolve, reject) => { + stream.setEncoding('utf8'); + stream.on('data', (chunk) => chunks.push(chunk)); + stream.on('error', (err) => reject(err)); + stream.on('end', () => resolve(chunks.join(''))); + }), + data = await s3Client.send(new GetObjectCommand(params)); +return streamToString(data.Body); +``` + +## Client initialisation is different + +Both v2 service objects and v3 clients can be customised with initialisation parameters, but there are subtle differences. For example, passing a `logger` object to the v2 service objects would provide amazingly useful logs before and after a call, with statistics. This saved me a ton of time troubleshooting problematic calls. The v3 SDK has a `logger` parameter, but only logs after a successful call, and without first turning it into JSON (so complex objects come out as `[Object object]`). In case of errors, when it's the most useful to have a log, v3 service clients log nothing. In case of stalled connections, v2 logs would show clearly where the client got stuck, but v3 logs show nothing. With v3 middleware injection, it's possible to replicate the useful v2 logger, but I'm hoping that someone at AWS will improve the basic logging in the future. + +Another common customisation for SDK clients are HTTP parameters, especially timeouts. Those have now moved to a separate class (`NodeHttpHandler`), and need to be passed as `requestHandler` instead of `httpOptions`. Here are the equivalent snippets: + +```js +//v2 +const aws = require('aws-sdk'), + s3 = new aws.S3({ + logger: console, + httpOptions: {timeout: 10000, connectTimeout: 1000} + }); + +//v3 +const { S3Client } = require('@aws-sdk/client-s3'), + { NodeHttpHandler } = require('@aws-sdk/node-http-handler'); + requestHandler = new NodeHttpHandler({ + connectionTimeout: 1000, + socketTimeout: 10000 + }), + s3Client = new S3Client({ + logger: console, + requestHandler + }); +``` + +One particularly problematic aspect of the new initialisation is how it handles the `endpoint` argument. Both v2 and v3 allow specifying an alternative endpoint in the constructor, which is useful for testing and to get management APIs working (for example, for posting to websockets using the API Gateway Management API). However, v2 SDK can take the API stage as part of the endpoint as well, and v3 SDK ignores the stage and only keeps the hostname. Unfortunately, for websocket APIs created with a stage, the correct stage has to come before the request path for posting to websocket connections. That results in `PostToConnectionCommand` being completely broken out of the box (it reports a misleading `ForbiddenException`). We lost a good few hours on this one, trying to identify differences IAM permissions, only to realise that it's a difference in how SDK handles endpoints. + + +I assume this will be changed at some point, because the only way to post to web sockets now with SDK v3 is to patch the request paths with a middleware. (There's an [active issue on GitHub about this](https://github.com/aws/aws-sdk-js-v3/issues/1830)). For anyone else hopelessly fighting with phantom `ForbiddenException` errors, here's the code to make it work. It expects the endpoint to have a stage at the end (eg produced by CloudFormation using `https://${ApiID}.execute-api.${AWS::Region}.amazonaws.com/${Stage}`). + +```js +const {PostToConnectionCommand, ApiGatewayManagementApiClient} + = require('@aws-sdk/client-apigatewaymanagementapi'), + path = require('path'), + client = new ApiGatewayManagementApiClient({endpoint, logger}); + // https://github.com/aws/aws-sdk-js-v3/issues/1830 + client.middlewareStack.add( + (next) => async (args) => { + const stageName = path.basename(endpoint); + if (!args.request.path.startsWith(stageName)) { + args.request.path = stageName + args.request.path; + } + return await next(args); + }, + { step: 'build' }, + ); +await apiClient.send(new PostToConnectionCommand({ + Data: JSON.stringify(messageObject), + ConnectionId: connectionId +})); +``` + +## Exception structure is different + +Lastly, v2 SDK throws exceptions with a `code` field in case of errors, that was useful for detecting the type of the error. That no longer exists in `v3`. Instead, check the `name` field. + +```js +try { + await apiClient.send(new PostToConnectionCommand({ + Data: JSON.stringify(messageObject), + ConnectionId: connectionId + })); +} catch (e) { + //v2 + if (e.code === 'GoneException') { return; } + //v3 + if (e.name === 'GoneException') { return; } + throw e; +} +``` + + +## To Migrate or Not to Migrate + +Version 3 SDK offers some clear advantages for front-end code (namely smaller bundles), and a cleaner async API, but it seems like it's a bit of a step back in terms of developer productivity. Also, at the time when I wrote this (May 2021), it still had quite a few rough edges. Most of the stuff is there, but it's difficult to find good documentation easily. There are still no firm deadlines on deprecating v2, but since v3 exists now, that will have to happen sooner or later, so it's worth starting to think about migration. The nice thing about how v3 is packaged is that it can co-exist with older v2 code, since the Node modules are completely different. + +I'd suggest using v3 for any new code, and starting to move less critical items of old infrastructure, while being very careful about integration testing anything you switch over. Tiny subtle differences may surprise you. diff --git a/src/_sass/_book_layout.sass b/src/_sass/_book_layout.sass index 4b745e3..3e33b10 100644 --- a/src/_sass/_book_layout.sass +++ b/src/_sass/_book_layout.sass @@ -11,19 +11,25 @@ $title-color: #841c1c text-align: center text-transform: uppercase - +breakpoint(sm) - +font-size(60) + +breakpoint(s) + +font-size(40) label - font-size: 6rem !important + font-size: 6rem font-weight: 600 + +breakpoint(s) + +font-size(32) + h2 font-family: Alegreya font-size: 3rem font-weight: 600 text-align: center + +breakpoint(s) + +font-size(24) + img.cover display: block margin: 0 auto 40px diff --git a/src/_sass/_mixins.sass b/src/_sass/_mixins.sass index f47d4e0..7fa3475 100644 --- a/src/_sass/_mixins.sass +++ b/src/_sass/_mixins.sass @@ -1,4 +1,7 @@ @mixin breakpoint($size) + @if $size == s + @media (max-width: 768px) + @content @if $size == sm @media (min-width: 768px) diff --git a/src/_sass/partials/_single.sass b/src/_sass/partials/_single.sass index 2b5f4b9..55e0060 100644 --- a/src/_sass/partials/_single.sass +++ b/src/_sass/partials/_single.sass @@ -76,6 +76,16 @@ blockquote h2 +font-size(28) + table, td + +font-size(20) + td, th + padding: 5px + text-align: right + th + padding-left: 50px + thead tr + border-bottom: 1px solid black + .single-content-sidebar padding: 30px 8% @@ -260,4 +270,4 @@ pre +dark-mode background-color: $code-background-dark - border: 1px solid $black \ No newline at end of file + border: 1px solid $black diff --git a/src/img/change-seo.jpg b/src/img/change-seo.jpg new file mode 100644 index 0000000..ad7fe1e Binary files /dev/null and b/src/img/change-seo.jpg differ diff --git a/src/img/running-serverless-realtime-graphql-applications-with-appsync-cover.jpg b/src/img/running-serverless-realtime-graphql-applications-with-appsync-cover.jpg new file mode 100644 index 0000000..280be98 Binary files /dev/null and b/src/img/running-serverless-realtime-graphql-applications-with-appsync-cover.jpg differ diff --git a/src/running-serverless-graphql-with-appsync.md b/src/running-serverless-graphql-with-appsync.md index 5c0e446..003f8f4 100644 --- a/src/running-serverless-graphql-with-appsync.md +++ b/src/running-serverless-graphql-with-appsync.md @@ -1,7 +1,10 @@ --- layout: book-landing seriesTitle: "Running Serverless" -title: "Realtime GraphQL Applicatins with AppSync" +seriesSubTitle: "Realtime GraphQL Applications with AppSync" +title: "Running Serverless: Realtime GraphQL Applications with AppSync" +excerpt: "New book by AWS Heroes Aleksandar Simovic, Slobodan Stojanovic and Gojko Adzic. Learn how to build and operate responsive, collaborative applications at scale with AWS AppSync and GraphQL." +feature_image: running-serverless-realtime-graphql-applications-with-appsync-cover.jpg permalink: /running-serverless-realtime-graphql-applications-with-appsync/ --- @@ -9,13 +12,13 @@ permalink: /running-serverless-realtime-graphql-applications-with-appsync/ New book by AWS Heroes Aleksandar Simovic, Slobodan Stojanovic and Gojko Adzic. Learn how to build and operate responsive, collaborative applications at scale with AWS AppSync and GraphQL. -A book will be available in Q3 2021, subscribe below, and we'll notify you when the early release is ready. +The book will be available in Q3 2021, subscribe below, and we'll notify you when the early release is ready. This book will teach you how to build, test, and operate a GraphQL application using AWS AppSync and AWS Cloud Development Kit (AWS CDK). -Running Serverless: Realtime GraphQL Applications with AppSync +Running Serverless: Realtime GraphQL Applications with AppSync ## Table of Contents @@ -189,7 +192,7 @@ This book will teach you how to build, test, and operate a GraphQL application u
-

18 Processingtransientdatawithlocalresolvers

+

18 Processing transient data with local resolvers

Triggering custom notifications

@@ -242,6 +245,6 @@ This book will teach you how to build, test, and operate a GraphQL application u ## Subscribe and get notified -A book will be available in Q3 2021, subscribe below, and we'll notify you when the early release is ready. +The book will be available in Q3 2021, subscribe below, and we'll notify you when the early release is ready. diff --git a/src/unit-testing-appsync-apps.markdown b/src/unit-testing-appsync-apps.markdown new file mode 100644 index 0000000..f1bfbdc --- /dev/null +++ b/src/unit-testing-appsync-apps.markdown @@ -0,0 +1,392 @@ +--- +layout: post +title: "Chapter 6: Testing AppSync applications - Unit testing" +date: 2022-03-21 12:00:00 +0200 +author_name : Slobodan Stojanović +author_url : /author/slobodan +author_avatar: slobodan.jpg +twitter_username: slobodan_ +show_avatar: true +read_time: 18 +feature_image: running-serverless-realtime-graphql-applications-with-appsync-cover.jpg +show_related_posts: false +permalink: /unit-testing-appsync-apps/ +--- + +
+

This is an excerpt from Chapter 6 of Running Serverless: Realtime GraphQL applications with AppSync, a book by Gojko Adzic, Aleksandar Simović, and Slobodan Stojanović.

+ +

Our book is not published yet. Your feedback means a lot to us, so please fill out the short survey at the end of this article to help us polish the story.

+ +

Please don't share this article. We'll publish a sharable version of this article as soon as the early version of the book is ready.

+
+ +Creating the first VTL template wasn't as hard as Ant expected. However, the development cycle takes a lot of trial and error and Ant feels the progress is slow. It is easy to make mistakes, especially because Ant doesn't have experience with VTL. Mistakes are OK as long as he can catch and fix them fast. However, with AppSync and VTL templates, Ant needs to deploy the application, test it, debug the VTL output and dig through AppSync logs. + +VTL templates can contain a significant amount of business logic, so it's important to find a better way to write and debug them. Web apps for generating AppSync VTL templates, such as Graphboss, are excellent, but Ant still needs to copy and paste his templates to verify them. However, someone else can edit a template without testing it in one of these applications. + +Ant thought about what he usually did for non-serverless and non-GraphQL apps. + +He would run the application locally to confirm that everything works, and he would also write tests. A typical application often requires three types of tests: unit, integration, and end-to-end (Figure 6.1). Because Ant likes the idea of the Test Pyramid¹, he would write: + +1. Many unit tests to verify that his business logic works. These tests are fast and cheap because they test Ant's functions in isolation. +2. Some integration tests to ensure that integration between units works, for example, if the data is saved to the database correctly. +3. A few end-to-end tests to verify that the application works as expected. + +![Figure 6.1: Testing a typical three-tier application](/img/ch_testing_appsync/testing-non-serverless-app.png) +_Figure 6.1: Testing a typical three-tier application_ + +"There are two big questions at the moment. Can I run my AppSync app locally, and how do I test it? Let's start with the first one. My app is a GraphQL application, so I should be able to run it locally. But how do I do that with AppSync?" Ant wonders. "There's a quick way to find that out!" + +## Testing AppSync applications + +After searching the web for a few minutes, Ant sends a message to Claudia, asking if he could run his AppSync application locally. A moment later, she replies, "In theory, it is possible to run the app locally using the simulator from the [AWS Amplify](https://aws.amazon.com/amplify/), but it's tricky to set up. However, you can use AWS Amplify simulator to run your unit tests." + +"Wait, what's Amplify?" Ant asked. + +"Don't worry about Amplify right now," Claudia replies, "just use the simulator, and I'll explain more later." + +> ### AWS Amplify +> +> Amplify is a framework for building web apps on top of AWS consisting of many different tools which we will cover in the Introduction to AWS Amplify chapter. In this chapter we'll only focus on using the simulator tool for testing purposes. + +## What to test in AppSync applications + +Ant decides to start with the Amplify simulator for unit tests. But before he starts, he wonders what he should test in an AppSync application and if the testing automation pyramid still applies? + +Ant sent another message to Claudia: "Hey, but what should I test in an AppSync application?" + +"That's a good question!" Claudia replies. "Testing an AppSync application still requires all three types of tests (unit, integration, and end-to-end). However, AWS already tests some parts of your application, so the scope of your tests is slightly different. It's easier to explain this with a diagram. Give me a minute!" + +A few minutes later, Claudia sent a diagram (Figure 6.2) with the following explanation: + +1. With unit tests, you verify that your VTL templates render as expected. Use the AWS AppSync simulator to render your VTL templates with specified parameters and confirm the response. +2. AWS already tests part of the system, so writing integration tests for that part is either impossible or redundant. For example, AWS ensures that the communication between GraphQL endpoint and VTL templates works. +3. Your integration tests verify that integration between GraphQL and data sources works. For example, you should test if the data is stored to DynamoDB when sending a mutation. +4. As with any other application, an AppSync app benefits from end-to-end tests. + +![Figure 6.2: Testing an AppSync application](/img/ch_testing_appsync/testing-serverless-app.png) +_Figure 6.2: Testing an AppSync application_ + +"This is perfect, thanks!" Ant replies. "By the way, what's the difference between integration and end-to-end tests in an AppSync application? I need to send an HTTP request for integration tests, right?" + +"Correct!" replies Claudia. "Remember, the Testing Pyramid argues that end-to-end tests through the UI are: brittle, expensive to write, and time-consuming to run. So it suggests that you should have an intermediate layer of tests that have many benefits of end-to-end tests, but without the complexities introduced by UI frameworks." + +"Makes sense. Where should I start?" + +### Simulating AppSync for local tests + +You need the Amplify AppSync simulator for unit tests. It's part of the Amplify CLI, but you can install it as a separate package from [npm](https://www.npmjs.com/package/amplify-appsync-simulator). + +The Amplify AppSync simulator package is not built for independent use, so it has no documentation. You might need to dig through the source code to find how to use it and that Amplify can introduce breaking changes at any point. However, it's still worth using because it takes time and energy to write and maintain a VTL renderer with AppSync's utility functions. + +The AppSync simulator is written in TypeScript, making the simulator integration easier. It's a class with many methods. However, the best place to start is [the Velocity Template class](https://github.com/aws-amplify/amplify-cli/blob/master/packages/amplify-appsync-simulator/src/velocity/index.ts) in the AppSync simulator's "velocity" folder. The VTL simulator provides a template and input parameters and renders it. + +### AppSync simulator setup + +Ant opens his terminal, navigates to the Upollo project folder and installs the Amplify AppSync simulator with the following command: + +```bash +npm install amplify-appsync-simulator --save-dev +``` + +He picks his favourite Node.js testing library, [Jest](https://jestjs.io). Amplify AppSync simulator is a Node.js tool, so any testing library for Node.js works with it. He runs the following command in his terminal to install Jest and Jest TypeScript transformer: + +```bash +npm install jest ts-jest --save-dev +``` + +He also creates the Jest configuration file, named `jest.config.js`, in the root folder of the Upollo project. This configuration file is written in JavaScript instead of TypeScript because it tells Jest how to read TypeScript. Ant can write it in TypeScript, but that would require additional configuration. At the moment, he wants a simple setup to start writing tests as fast as possible. In his Jest configuration file, Ant writes the following: + +```javascript +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; +``` + +Jest knows that it needs to run the tests using the Node.js environment. It also knows that it can find tests in the `test` folder at the root of the project and that all tests have`.test.ts` extension. Finally, it knows that it needs to apply the Jest Typescript transformer to run all files with `ts` and `tsx` extensions. + +Then, Ant creates the `test` folder in the project root. The Amplify AppSync simulator library has a lot of boilerplate code, so Ant wants to write a small abstraction helper. He creates the `helpers` folder inside the `test` folder and a file named `vtl-simulator.ts` inside it. + +He opens the `vtl-simulator.ts` file in his VS code and writes the following lines to import the Amplify AppSync simulator and its velocity file as dependencies: + +```typescript +import { AmplifyAppSyncSimulatorAuthenticationType, AmplifyAppSyncSimulator } from 'amplify-appsync-simulator'; +import { VelocityTemplate, AppSyncVTLRenderContext } from 'amplify-appsync-simulator/lib/velocity'; +``` + +Ant's unit tests read VTL templates and GraphQL schema from the local disk, so he imports the `fs` and `path` modules: + +```typescript +import { readFileSync } from 'fs'; +import { join } from 'path'; +``` + +Ant learns from the documentation that he needs to provide the request context and request info for the Velocity render method. However, it seems that these don't affect the VTL templates he is planning to write, so he decides to make default values to omit to pass them in each unit test. He also imports the `API_KEY` type from the AppSync simulator to please the TypeScript type checking. As AppSync simulator types are not strictly defined, and Ant is lazy to spend a lot of time finding the correct types, he sets the `any` type in a few places. Then he creates a path to the GraphQL schema because he'll need to load it later for the AppSync simulator and ends up with the following lines of code: + +```typescript +const { API_KEY } = AmplifyAppSyncSimulatorAuthenticationType; + +const defaultRequestContext: any = { + headers: {}, + requestAuthorizationMode: API_KEY +}; +const defaultInfo: any = { + fieldNodes: [], + fragments: {}, + path: { + key: '', + } +}; +const defaultSchemaPath = join(__dirname, '..', '..', 'schema.graphql'); +``` + +Ant creates the `VTLSimulator` class, declares the `vtl` variable as `any`, and hopes that Claudia will never see these `any` types. + +```typescript +export class VTLSimulator { + vtl: any; + // TODO: Add constructor and render method +} +``` + +Then he writes the constructor of his new class that accepts the VTL template path as a parameter. The constructor also allows Ant's team to overwrite the path to the GraphQL schema if they need to pass a different one. + +Ant loads the VTL template and the GraphQL schema using the `readFileSync` function from the `fs` module in the constructor. He can read these files asynchronously, but this is a helper for his unit tests, so he decides to keep it as simple as possible. + +Then he creates the instance of the `AmplifyAppSyncSimulator` class and calls the `init` method to initialise the simulator. The `init` method requires a GraphQL schema and some basic AppSync settings, such as name and the default authentication type. At this point, Ant does not care about the authorisation type, so he passes the `API_KEY`. He'll change it later if he needs to. + +After initialising the AppSync simulator, Ant creates an instance of the `VelocityTemplate` class by passing the VTL template content and the simulator as parameters. + +```typescript + constructor(filePath: string, schemaFilePath = defaultSchemaPath) { + const content = readFileSync(filePath, 'utf8'); + const graphQLSchema = readFileSync(schemaFilePath, 'utf8'); + const simulator = new AmplifyAppSyncSimulator(); + simulator.init({ + schema: { + content: graphQLSchema, + }, + appSync: { + name: 'name', + defaultAuthenticationType: { + authenticationType: API_KEY + }, + additionalAuthenticationProviders: [] + }, + }); + this.vtl = new VelocityTemplate({ content }, simulator); + } +``` + +Ant also creates the `render` method in his `VTLSimulator` class. He puts the `templateParameters` as the only argument of this method. He'll use this argument to pass the part of the AppSync context he needs to render the template. + +In the render method, he uses the `templateParameters` to extend the default context, renders the template using the `VelocityTemplate` instance and the default values he defined, and returns the result. + +```typescript + render(templateParameters: Partial): any { + const ctxParameters = { source: {}, arguments: { input: {} }, ...templateParameters }; + return this.vtl.render( + ctxParameters, + defaultRequestContext, + defaultInfo + ); + } +``` + +The new `VTLSimulator` helper should make his unit tests clean and simple. However, his linter complains about the `any` types. He puts the following line at the top of his new helper file: + +```typescript +/*eslint @typescript-eslint/no-explicit-any: "off" */ +``` + +"I really hope that Claudia will never see this," Ant thinks while he proudly looks at his VTL Simulator helper with a smile on his face. + +### Creating Unit tests for VTL templates + +"Where should I start with my unit test for the `get-survey-by-id-request.vtl` template?" Ant wonders while grinding coffee beans for a new cup of espresso. "At some point, Claudia mentioned that I should check for the errors first. That sounds like a good idea." He would do the same for non-serverless applications. "How do I check the errors?" + +After a few trials and errors, Ant finds out that the AppSync VTL engine returns the following values when it renders a template: + +- `errors` - a list of errors that occurred during rendering. +- `hadExceptions` - a boolean that indicates if the rendering had errors or not. +- `isReturn` - a boolean value that indicates if the result is a return value or not. +- `stash` - a map of stashed values, available during a single resolver execution. +- `result` - a rendered template result. + +Ant can test the errors by verifying that the `hadExceptions` value is set to `false`, or he can check if the `errors` array is empty. Checking both values sounds redundant. The second one sounds easier because he can also check if the `errors` array contains a specific error when he throws one. + +After trying a few examples with the local renderer and a deployed application, Ant sees some differences between his simulator and an actual AppSync VTL engine. These differences are not blocking at the moment. For example, in the AppSync VTL engine, the `isReturn` value is `true` if the response resolver template is rendered. The local simulator does not know if the template is a request or response resolver template, so the only way to make the `isReturn` value true is to use [the `#return` directive](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-util-reference.html#aws-appsync-directives). + +"Now I understand why Claudia mentioned that my unit tests are as good as my mocks." Ant nods. "These unit tests can help us write or edit VTL templates faster and catch some important issues. However, we'll always need to verify them in the deployed AppSyn API." + +Testing the `isReturn` value doesn't make much sense. However, the `stash` value sounds like a good candidate for testing. Ant can verify if the stash is empty or not. He is not using it now, but he will use it for sure when he starts writing more complicated VTL templates. + +Finally, Ant needs to verify that the `result` value actually contains the rendered value he expects. There are also minor differences between the simulated `result` value and the actual AppSync `result` value. For example, the AppSync VTL engine always transforms text to text, but the local simulator converts the JSON values to a JavaScript object. Which makes them easier to verify in Jest. + +Ant's verification process looks similar to Figure 6.3. + +Figure 6.3: VTL renderer result verification process +_Figure 6.3: VTL renderer result verification process_ + +Ant needs to test both his request and response mapping templates. + +### Testing request mapping templates + +Ant starts with the request mapping template. He creates a new file in his `test` folder, and names it `get-survey-by-id-request.vtl.test.ts`. + +He opens the new test file in VS Code, and imports the VTL simulator from `helpers` folder. + +```typescript +import { VTLSimulator } from './helpers/vtl-simulator'; +``` + +Ant imports the `join` function from the `path` module. He uses the `join` function to create an absolute path to the VTL template file he wants to test and uses that path to create the instance of the VTL simulator class. + +```typescript +import { join } from 'path' +const templatePath = join(__dirname, '..', 'vtl', 'get-survey-by-id-request.vtl') +const velocity = new VTLSimulator(templatePath) +``` + +He creates an empty `describe` block for his tests and names it `get-survey-by-id-request.vtl`. This name is not creative, but it's descriptive, so he likes it. + +```typescript +describe('get-survey-by-id-request.vtl', () => { + // TODO: Write unit tests +} +``` + +Ant is finally ready to write his first unit tests. + +"Where should I start?" he wonders. "My request template is simple. I can test what happens when I pass a correct ID and an incorrect ID. My GraphQL schema will make sure that I always have the ID and that it is a string, so I do not need to test if the ID exists or not. I'll start with a test that should return the survey with the ID I provided." + +He creates a first test inside the `describe` block and names it "should return the survey with the provided ID." + +At the beginning of the test, he creates a context object with the following arguments: `{ "id": "First" }`. Then he passes the test context to the `velocity.render` method to render a template and stores the result in the `rendered` variable. + +Finally, he checks the `rendered.errors` and `rendered.results` values. The `errors` array should be empty, and the result should have a `payload` that represents the survey with the ID "First," and a version equal to `"2018-05-29"`. + +```typescript + test('should return the survey with the provided ID', () => { + const ctxValues = { arguments: { id: 'first' } } + const rendered = velocity.render(ctxValues) + expect(rendered.errors).toEqual([]) + expect(rendered.result).toEqual({ + payload: { + id: 'first', + question: 'What is the meaning of life?', + }, + version: '2018-05-29', + }) + }) +``` + +"That wasn't hard. I hope it works! But let's write the other one before running them." + +He creates another test in the `describe` block and names it "should not return payload when survey is not found." + +The second test is similar to the first one. Ant creates a test context, with a crucial difference -- the ID is `"x"`, and then uses the `velocity.render` method to render the template. Then he checks that there are no errors and that the result has a version without a payload. + +"Maybe I should throw an error when the survey with an ID is not provided. That would improve both my code and my tests. However, this is just a test resolver, so I'll make sure to throw a meaningful error when I connect the resolver to a database." + +```typescript + test('should not return payload when survey is not found', () => { + const ctxValues = { arguments: { id: 'x' } } + const rendered = velocity.render(ctxValues) + expect(rendered.errors).toEqual([]) + expect(rendered.result).toEqual({ + version: '2018-05-29', + }) + }) +``` + +Ant opens his terminal and navigates the project folder. Then he runs the `npm test` command to run his tests. Jest is running. One second. Two. Three. Five. Eight. Suspension is killing him. And finally, after almost 13 seconds, he sees the green `PASS` on his screen. Both tests passed with the following feedback in his terminal: + +```bash + PASS test/get-survey-by-id-request.vtl.test.ts (12.433 s) + +Test Suites: 1 passed, 1 total +Tests: 2 passed, 2 total +Snapshots: 0 total +Time: 13.383 s +Ran all test suites. +``` + +"Woohoo! It works." + +### Testing response mapping templates + +"Let's do it again," Ant thinks while he creates another file in his `test` folder and names it `get-survey-by-id-response.vtl.test.ts`. "I can test the error first for the response template." + +He opens the new file in his VS Code and imports his VTL simulator and response VTL template at the top, creating an instance of the VTL simulator class. + +```typescript +import { join } from 'path' +import { VTLSimulator } from './helpers/vtl-simulator' +const templatePath = join(__dirname, '..', 'vtl', 'get-survey-by-id-response.vtl') +const velocity = new VTLSimulator(templatePath) +``` + +Then he creates a new `describe` block with the name "get-survey-by-id-response.vtl." Ant writes the first test "should return an error if survey is not found." Then he creates an empty context object and renders a template with it. Ant expects that the `errors` array contains the "Survey not found" error. The `result` should be an empty object. + +He creates another test: "should return the survey with the provided ID." In this test, he checks that the errors array is empty and that the result is equal to the result he passes in the arguments: `{ something: true }`. + +```typescript +describe('get-survey-by-id-response.vtl', () => { + test('should return an error if survey is not found', () => { + const ctxValues = {} + const rendered = velocity.render(ctxValues) + expect(rendered.errors).toEqual([ + new Error('Survey not found.'), + ]) + expect(rendered.result).toEqual({}) + }) + + test('should return the survey with the provided ID', () => { + const ctxValues = { result: { something: true } } + const rendered = velocity.render(ctxValues) + expect(rendered.errors).toEqual([]) + expect(rendered.result).toEqual({ + something: true + }) + }) +}) +``` + +Ant opens his terminal, runs the `npm test` command again, and sees two green "PASS" statuses 10 seconds later: + +```bash + PASS test/get-survey-by-id-request.vtl.test.ts + PASS test/get-survey-by-id-response.vtl.test.ts (9.427 s) + +Test Suites: 2 passed, 2 total +Tests: 4 passed, 4 total +Snapshots: 0 total +Time: 9.924 s, estimated 13 s +Ran all test suites. +``` + +A few minutes later, Ant sends a message to Claudia to brag about his new testing skills. + +"Bravo! But don't pop the champagne bottle yet," Claudia says with a smiley, "let's write some end-to-end tests first." + + +> ### Integrating tests with build pipelines +> +> In this chapter, Ant will learn how to test an AppSync application. He'll integrate automated tests with their build pipelines to complete the test automation in the Working with deployment pipelines chapter. + +------ + +
+

This is an excerpt from Chapter 6 of Running Serverless: Realtime GraphQL applications with AppSync, a book by Gojko Adzic, Aleksandar Simović, and Slobodan Stojanović.

+ +

Here's a short survey that will help us to polish the story. It'll take two minutes or less to fill it out. Thanks!

+
+ +¹ Test Pyramid, or Test Automation Pyramid, was introduced by Mike Cohn in his book Succeeding with Agile \ No newline at end of file