Use Cypress to test AWS Amplify Apps with Authentication

TL;DR - E2E test your AWS Amplify application without worrying about your authentication!

  • You protected your app with authentication using the withAuthentication() HOC or <AmplifyAuthenticator>.
  • You are using Cypress to build E2E tests
  • You're on the right track! emoji-smile

Challenge: Before every test scenario you must now sign in! How am I going to deal with that?! emoji-flushed


You don't want to use your UI to login before every test scenario!

If you would use your UI to login before every test scenario you'd have to clutter every test scenario with code to automatically fill in your credentials.

We want a simple cypress command cy.signIn() that does the work for us!

This blog shows you how to do that and has a link to a github repo containing the example used throughout the blog.

Prerequisits

  • Create a test user in your cognito user pool and remember the user and password.
  • Your package.json contains:

    "devDependencies": { "cypress": "^5.3.0", "cypress-localstorage-commands": "^1.2.2", "aws-amplify": "^3.3.4" }
  • Your cypress.json contains:

    { "baseUrl": "http://localhost:3000", "includeShadowDom": true }

Configuration

In the root of your project create a file cypress.env.js. Add the file to your .gitignore 'cause you don't want to commit your credentials.

In this file configure your username, password, userPoolId and clientId. The first two you created when creating your user in the userpool. The last two you can find in the aws-exports.js file.

cypress.env.js:

{ "username": "yourUserame", "password": "yourPassword", "userPoolId": "eu-west-1_BYZ5DzkpO", "clientId": "79audohaaahb432ovoa240asaj" }

Custom commands can by created in cypress/support/commands.js In commands.js add the following configuration code:

const Auth = require("aws-amplify").Auth; import "cypress-localstorage-commands"; const username = Cypress.env("username"); const password = Cypress.env("password"); const userPoolId = Cypress.env("userPoolId"); const clientId = Cypress.env("clientId"); const awsconfig = { aws_user_pools_id: userPoolId, aws_user_pools_web_client_id: clientId, }; Auth.configure(awsconfig);

Create custom Cypress signIn command

Cypress allows us to create custom Cypress commands.
That means, awesome... we can create our custom signIn command!
As we said before, you can configure custom commands in cypress/support/commands.js

Amplify, what else? emoji-smile

We will use the Amplify javascript library to aid us with the authentication.
Look at that: commands.js:

Cypress.Commands.add("signIn", () => { cy.then(() => Auth.signIn(username, password)).then((cognitoUser) => { const idToken = cognitoUser.signInUserSession.idToken.jwtToken; const accessToken = cognitoUser.signInUserSession.accessToken.jwtToken; const makeKey = (name) => `CognitoIdentityServiceProvider.${cognitoUser.pool.clientId}.${cognitoUser.username}.${name}`; cy.setLocalStorage(makeKey("accessToken"), accessToken); cy.setLocalStorage(makeKey("idToken"), idToken); cy.setLocalStorage( `CognitoIdentityServiceProvider.${cognitoUser.pool.clientId}.LastAuthUser`, cognitoUser.username ); }); cy.saveLocalStorage(); });

How does this work? emoji-bulb

  • We use the amplify Auth class to create a session for our user. This command returs a CognitoUser.
  • The Auth.singIn() command needs to be wrapped in cy.then() because of the way cypress handles promises.
  • Extract the idToken and accessToken from the cognitoUser.
  • The cognito credentials need to have a specific format, hence the makeKey function.
  • Use cy.setLocalStorage to save the credentials to localStorage. The setLocalStorage function originates from the cypress-localstorage-commands dependency that we installed via our dev dependencies.
  • We save our localStorage using cy.saveLocalStorage.

Sign In before your test!

Before running a test spec we will have to signIn and preserve our localStorage during the run of these tests.
After running all tests in the spec we want to clear localStorage since we don't want to build up any state between tests.
Here is an example of how to do that using the before(), beforeEach, after() and afterEach() functions.

describe("Example test", () => { before(() => { cy.signIn(); }); after(() => { cy.clearLocalStorageSnapshot(); cy.clearLocalStorage(); }); beforeEach(() => { cy.restoreLocalStorage(); }); afterEach(() => { cy.saveLocalStorage(); }); it("should be logged in", () => { cy.visit("/"); cy.get(".App-logo").should("be.visible"); it("Should talk about react", () => { cy.visit("/") cy.contains("React") }) }); });

End result!

Start your app: npm start.
In another terminal open you cypress tests: npm run cypress:open.
When we run our test suite we no longer have to worry about authenticating in the UI!


Resources

Want to try it out yourself!?
Checkout this repository to play with the example that was featured here: https://github.com/Nxtra/cypress-amplify-auth-test.

Credits

Featured image by iMattSmart on Unsplash.