Skip to content

matthewburfield

moon indicating dark mode
sun indicating light mode

Amplify continuous integration/deployment with end-to-end tests

July 16, 2020

sunrise Photo by Cristina Gottardi on Unsplash

Introduction

I recently deployed my AWS Amplify app, and added end-to-end (E2E) testing with cypress in the build pipeline. This means everytime I push a commit to GitHub, Amplify will automatically provision and build my app, and then run any E2E tests.

If a test fails, then Amplify will stop the pipeline and won’t deploy. You can also set up alerts so you get an email or slack notification when the pipeline fails.

failed pipeline A failed pipeline build in AWS Amplify

If everything passes, then it will deploy the app as well! This my friends, is continuous integration and deployment (CI/CD) and if you haven’t seen it in action before, it’s pretty sweet.

passing pipeline A passing and completed build in AWS Amplify

I spent a fair amount of time getting this working, and many of the resources I found online (like this tutorial), were all out of date. So I decided to write a new tutorial - I hope it helps!

Let’s get started.

Set up a project locally

I’ll be using create-react-app for my app, but you can substitute that with whichever framework you want. I also like to use yarn as my package manager, but if you prefer npm, just substitute any yarn commands with the npm equivalent.

npx create-react-app amplify-e2e
cd amplify-e2e

Now initialize Amplify by running the npx amplify init command. I’ve added all the responses that I entered, but again, substitute any that you need to match your environment. One thing to note is that I already had an AWS profile set up on my machine, so your last two questions might look a bit different if you don’t and need to set one up.

yarn global add @aws-amplify/cli
amplify init

? Enter a name for the project (amplifye2e)
? Enter a name for the environment (dev)
? Choose your default editor: (Visual Studio Code)
? Choose the type of app that you're building (javascript)
Please tell us about your project
? What javascript framework are you using (react)
? Source Directory Path (src)
? Distribution Directory Path (build)
? Build Command (yarn build)
? Start Command (yarn start)
? Do you want to use an AWS profile? (Yes)
? Please choose the profile you want to use (amplify-cli)

Now add authentication to your backend.

amplify add auth

? Do you want to use the default authentication and security configuration? (Default configuration)
? How do you want users to be able to sign in? (Username)
? Do you want to configure advanced settings? (No, I am done.)

amplify push

Now let’s add an authentication component to our app! Amplify has pre-built UI components for React, Vue, Angular and React Native. You can read more about them in the Amplify documentation.

yarn add aws-amplify @aws-amplify/ui-react

Update your /src/App.js with the following changes:

/src/App.js
import React from "react";
import logo from "./logo.svg";
import "./App.css";

import Amplify from "aws-amplify";import { withAuthenticator, AmplifySignOut } from "@aws-amplify/ui-react";import aws_exports from "./aws-exports";Amplify.configure(aws_exports);
function App() {
  return (
    <div className="App">
      <AmplifySignOut />
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default withAuthenticator(App);

Now let’s run our project locally with the yarn start command.

yarn start

With that, you should see a browser window open with the Amplify Auth UI. Create a new account and log in. Remember the login details as you will use this account later for our E2E test!

Creating a new user Creating a new user locally

Side note - View your user in Cognito

Just a quick side note as this took me a while to figure out as well. If you’re new to Amplify, you might wonder where the user you just created is stored. You can see the users for your Amplify apps by logging into the AWS console and going to the Cognito service. Click on “manage user pools” and then the name of your app. Amplify creates the name for you, mine’s called: “amplifye2ef97ea632_userpool_f97ea632-dev”. Click on that.

You should be presented with this screen:

Screenshot of AWS console - user pool Your app’s AWS user pool

Then click on “Users and groups” in the left hand menu which will take you to your users.

Screenshot of AWS console - app users Your app’s AWS users

Create your first E2E test with Cypress

Cypress is a popular JavaScript-based testing framework for running E2E tests in the browser. Let’s first install Cypress.

yarn add cypress --dev

When we first load Cypress, Cypress will initialize some files within our app - we’ll take a look at these in a second. First, let’s run Cypress on our app.

yarn start // (only if your local dev server isn't already running)
yarn cypress open

Loading screen of cypress on first load Loading screen of Cypress on first load

Here we can see the files that Cypress has injected into our app. They include some sample tests, and some configuration. For now we can just click the “OK, got it!” button.

Now that cypress has been initialized, feel free to click on the “Run all specs” button if you’ve never used Cypress before and want to see it in action, otherwise you can move on :)

Bonus - Preparing Cypress to record to the Cypress dashboard

Cypress has a dashboard that you can use to report on all of your test runs!

Screenshot of cypress dashboard Screenshot of the Cypress dashboard

We can set up our project with Cypress now so later on when we are running our tests they save to the dashboard.

Back in our Cypress test runner window, click on the “Runs” tab, and then click on “Setup project to record”. Cypress should guide you though a couple of steps to get setup.

The runs tab of cypress window The “Runs” tab of Cypress window

Setting up your cypress project Setting up your Cypress project

Cypress project confirmation window Cypress project confirmation window

In order to have Amplify push the results of your tests to the dashboard, you need to give it the “key” which I prefer to add as an environment variable in the AWS console. Logging into the AWS amplify console and clicking on “Environment variables” in your app settings, create an environment variable called “CYPRESS_RECORD_KEY” and attach your key.

Adding environment variables to the AWS console Adding environment variables to the AWS console

Cypress setup

First we need to update the /cypress.json file to add some configuration settings. We want to add a base url so that in our tests, we don’t need to type out “http://localhost:3000” every time, for each test. So let’s add that now.

If you followed along on the previous steps, you should see cypress has already added your project id. If it hasn’t, just run the cypress test runner again and get it from the “Runs” tab like in the previous step.

cypress.json

{
  "projectId": "ecbjt5",
  "baseUrl": "http://localhost:3000"}

Note that I’m using port 3000 because that’s what create-react-app uses by default. If you’re hosting your app somewhere else, just adjust your baseUrl accordingly.

Next, I like to add all my e2e tests to a folder named “e2e”, instead of the default “integration” folder that cypress creates. (Thanks to Kent C. Dodds for this tip). We can update the cypress.json file to accommodate this:

cypress.json

{
  "baseUrl": "http://localhost:3000",
  "projectId": "ecbjt5",
  "integrationFolder": "cypress/e2e"}

There’s one last piece of configuration we need to do before we can test the authentication flow. Since Amplify updated it’s UI components to the @aws-amplify/ui-react library, it now uses a shadow DOM to inject the Authentication UI which makes it a little trickier to test because the standard query selectors won’t work.

Fortunately, as of version 4.8.0, cypress has added an experimental feature to support the shadow DOM! We just need to set one more flag in our cypress.json file to enable it.

cypress.json

{
  "baseUrl": "http://localhost:3000",
  "projectId": "ecbjt5",
  "integrationFolder": "cypress/e2e",
  "experimentalShadowDomSupport": true}

That’s all the configuration we need to get rolling. Now let’s go add some E2E tests!

Creating our E2E tests

We need to create a new folder that matches our new integrationFolder path that we just created, so inside the cypress directory, create a new folder called “e2e”. Inside the “e2e” folder, let’s create our first test, authenticator.spec.js

This should give you a project file structure like this.

- amplify-e2e
  - amplify
  - cypress
    - e2e// highlight-line
      `authenticator.spec.js`// highlight-line
    - fixtures
    - integration
    - plugins
    - support
  - node_modules
  - public
  - src
    .gitignore
    cypress.json
    package.json
    README.md
    yarn.lock

Here is the code for your first test. I’ve tried to add explanations in the comments

/cypress/e2e/authenticator.spec.js
// Update these constants to match whatever username and password you
// created your user with
const USERNAME = "UPDATE_USER_NAME_HERE";
const PASSWORD = "UPDATE_PASSWORD_HERE";

describe("Authenticator:", function () {
  // Step 1: before each test, we want to visit our app.
  // Note that this takes into account the base url that we
  // already configured. So the final address will be
  // "http://localhost:3000/"
  beforeEach(function () {
    cy.visit("/");
  });

  describe("Sign In:", () => {
    it("allows a user to signin", () => {
      // Step 2: Usually we can use cy.get to go locate the
      // button or field that we want to action directly, but
      // because of the shadow DOM, we can only use cy.get on
      // elements outside any shadow DOM. So here we get the element
      // outside the shadow DOM, and then use cy.find command, passing
      // in the "includeShadowDom: true" flag, to tell cypress to look
      // inside the shadow DOM.

      // Cypress commands are all asynchronise and can be chained together.
      // Which is what I'm doing here. If you want, you can read more about
      // it in the cypress documentation:
      // https://docs.cypress.io/guides/getting-started/writing-your-first-test.html#Add-a-test-file
      cy.get("amplify-authenticator")
        .find(selectors.usernameInput, {
          includeShadowDom: true,
        })
        .type(USERNAME);

      cy.get("amplify-authenticator")
        .find(selectors.signInPasswordInput, {
          includeShadowDom: true,
        })
        .type(PASSWORD, { force: true });

      // This one was a bit tricky to get to. It seems as though
      // there are two sign in buttons and cypress doesn't like
      // trying to click on two buttons at the same time,
      // so I'm chaining the .first() method to just take the
      // first button it finds.
      cy.get("amplify-authenticator")
        .find(selectors.signInSignInButton, {
          includeShadowDom: true,
        })
        .first()
        .find("button[type='submit']", { includeShadowDom: true })
        .click({ force: true });

      // Step 3: Make an assertion (Check for sign-out text)
      // By this stage we should be logged in. Cypress has many different
      // assertions. Here we're just checking to see if the page contains
      // the sign-out button.
      cy.get("amplify-sign-out")
        .find(selectors.signOutButton, { includeShadowDom: true })
        .contains("Sign Out");
    });
  });
});

export const selectors = {
  // Auth component classes
  usernameInput: 'input[data-test="sign-in-username-input"]',
  signInPasswordInput: 'input[data-test="sign-in-password-input"]',
  signInSignInButton: 'amplify-button[data-test="sign-in-sign-in-button"]',
  signOutButton: "amplify-button",
};

Now we can run cypress again and (fingers crossed) we should see our test pass!

Run cypress again by running the command

yarn cypress open

And you should see your authenticator.spec.js test ready to go. If you are still seeing all the example tests that cypress added or not seeing any tests, go back and look at the configuration steps. Chances are your “integrationFolder” configuration doesn’t match the path that you added your authenticator.spec.js test.

Hopefully we now have a passing test! Whoop whoop.

Set up continuous delivery with the Amplify Console

The Amplify console provides a continuous deployment and hosting service for our amplify apps. To get started with our continuous deployment, let’s first push our code to GitHub (or the git provider of your choice).

I created a new repo on GitHub called “amplify-e2e” and then ran these commands. Obviously change the origin to point to your own repo.

git add .
git commit -m "initialised app and added auth with e2e test"
git remote add origin https://github.com/Matthew-Burfield/amplify-e2e.git
git push origin master

Now log into the AWS amplify console and you should see your app there. Note that if your app isn’t there, you might need to switch to your region to the one you set up amplify with.

Clicking through to your app will show you a list of git providers. I’m selecting GitHub since that’s where I pushed my code to, you can select whichever git provider you used.

Screenshot of the list of repository providers in AWS console The list of repository providers in AWS console

Click “connect branch”

If you haven’t already authorized amplify to your repository service provider, amplify will prompt you to do that, then select your repo and branch and click next.

Screenshot of selecting the repo and branch Selecting your repo and branch

The next screen will be the build settings configuration. If you’ve followed all the steps so far, amplify should automatically detect you’re using React for your frontend framework, and cypress for your testing framework.

What we’re interested in here is the “Build and test settings”.

Screenshot of build and test settings in AWS console The default build and test settings

Most of it works out of the box, but there are a few things we need to update in for our create-react-app app, and to record our test results on the cypress dashboard.

Replace your build and test settings with this:

version: 0.1
backend:
  phases:
    build:
      commands:
        - "# Execute Amplify CLI with the helper script"
        - amplifyPush --simple
frontend:
  phases:
    preBuild:
      commands:
        - yarn install --frozen-lockfile
    build:
      commands:
        - yarn build
  artifacts:
    baseDirectory: build
    files:
      - "**/*"
  cache:
    paths:
      - node_modules/**/*
test:
  artifacts:
    baseDirectory: cypress
    configFilePath: "**/mochawesome.json"
    files:
      - "**/*.png"
      - "**/*.mp4"
  phases:
    preTest:
      commands:
        - yarn install --frozen-lockfile
        - yarn add mocha mochawesome mochawesome-merge mochawesome-report-generator
        - "npx serve -l 3000 -s build & npx wait-on http://127.0.0.1:3000"
    test:
      commands:
        - 'npx cypress run --record --reporter mochawesome --reporter-options "reportDir=cypress/report/mochawesome-report,overwrite=false,html=false,json=true,timestamp=mmddyyyy_HHMMss"'
    postTest:
      commands:
        - npx mochawesome-merge@4 cypress/report/mochawesome-report/*.json > cypress/report/mochawesome.json
        - npx kill-port 3000

There are a few things that have changed from the default, and a few things that have stayed the same. Let’s just go through each step together.

The first step is the backend. Amplify uses the Amplify CLI to run “amplifyPush —simple” which will do all the setup for you for any amplify services you are using. It also re-creates the aws-exports.js file for you, since we don’t push it to our git repository because it contains sensitive information.

Next is the frontend.

the preBuild and build commands should be pretty self explanatory, it’s just installing any node_modules via yarn install, and then building the project with the yarn build command.

One thing you might wonder about is the --freeze-lockfile flag that is passed to yarn install. It’s basically the equivalent of the npm ci command that was in the default yml file. It prevents any more package changes from happening.

According to the yarn documentation - “If you need reproducible dependencies, which is usually the case with the continuous integration systems, you should pass —frozen-lockfile flag.

The artifacts will be the built files that we want to deploy. create-react-app puts all our build files in a directory called “build” for us, so we specify that here. Along with the fact that we want to deploy all the files in that folder.

Finally is the test phase.

Artifacts work the same as the frontend phase above.

Let’s check out the preTest, test and postTest commands since there were a couple of changes here.

The first two commands (yarn install and yarn add) are exactly the same.

The third command has changed. We can’t use the yarn start command here because that starts the webpack dev server from react-scripts. Instead we can create a static server with our production build that we can test against. Remembering that by the time CI runs this script, a successful build will have already been completed - it makes sense to test against the build we are just about to deploy. If you want to learn more about the serve command, you can see it in the create-react-app docs

The test command has stayed the same, except I’ve also added the “record” flag to tell it to record the tests and send to the cypress dashboard; Note that we don’t need to pass the “key” flag, because we’ve already added our key as an environment variable.

And that’s it for the build file!

One last thing to do in build settings, is to update the build image settings. Amplify gives you a list of live package updates that it will maintain for you in your build image. I’ve added yarn and cypress to be the latest versions.

Amplify console build image settings Amplify console build image settings

Good job!!

We’re done! You can now redeploy from the amplify console, or simply make an update locally and commit/push to your git repo and Amplify will see the new commit come through and redeploy.

Screenshot of the cypress dashboard with first test results Screenshot of the cypress dashboard with first test results

Take a sip of coffee and watch on the Amplify console as it builds, tests and deploys your project for you! ☕️✌

Conclusion

Phew! That was a lot. Take a minute and think about everything we just did.

We successfully set up a pipeline in Amplify that will continuously build and deploy our app. Not only that, but it will also run any end-to-end tests that we have using Cypress, and output those tests to the Cypress Dashboard!

Having this infrastructure in place will enable us to develop with speed and confidence knowing that our apps are fully tested before deployment!

Happy days.

Thanks for reading, and until next time,

Happy Coding!


Hi, I'm Matthew Burfield. I like writing about cool development stuff.