Writing integration tests with Cypress for React app behind Okta authentication

How to test UI in a box

Lula Leus
Product and Engineering at Condé Nast
7 min readNov 11, 2019

--

Introduction

Recently I worked on a new front-end feature for the project at work. I was ready to declare my work completed when I noticed that one of the tests, not even related to the feature I developed, is failing.

I am used to Jest tests failing in React apps because I forgot to pass a new prop to a component in test, to update a snapshot or whatever. So I was really surprised when I found out that Cypress, the test runner I recently added to the project testing toolset, caught an actual bug which I introduced during my feature development and failed to notice.

I was so excited with this proof that Cypress prevented me from shipping bugged code, that I decided to write a post about how to start testing React app, complicated by Okta authentication, with Cypress. Actually… it is really easy to do.

Overview of Front end testing strategies

Nowadays, there are many ways to test Front end applications. Let’s reiterate on some of them and their use-cases.

  • Unit tests. They help enormously with writing code with testability in mind. They keep us confident that small units of functionality are working correctly. Unfortunately, the usefulness of unit tests is low after the development stage is completed. The defacto default way to write unit test for React app today is Jest plus Enzyme or a newer React-testing-library. I personally still prefer Enzyme because it has richer toolset.
  • Visual regression tests. They help to avoid visual differences in how UI looks. Percy.io is the tool I use at work for this kind of tests.
  • End-to-end tests are undoubtedly effective, but time-consuming and complicated to setup. A good way to start with the e2e is Puppeteer.
  • Integration tests. It is the type of tests I will concentrate on in this article, and more precisely on how to write them with Cypress.

Integration tests in more details

The definition of integration tests is vague. Even the following most basic example, written with Enzyme, can be considered an integration test.

The test asserts that when user clicks a button in `ParentComponent`, `ChildComponent` is rendered within it. When another button, in `ChildComponent`, is clicked, the `ChildComponent` will be removed from the DOM.

So we wrote an integration test. Integration between two components only, but integration nonetheless. Great? Not really.

There are two major problems, which may look insignificant in this tiny example but become real obstacles in more complex scenarios.

First —In the test, we are constructing a chain of user interactions with visual elements without actually seeing them. I need to imagine these two components in DOM, the buttons, appearance, and disappearance of a piece of HTML in my head. It is feasible with two component example, but orchestrating more complex user interactions without seeing them does not sound like a great idea. Even worse, what if someone else constructed such a test some time ago? And you are only trying to understand what change broke this invisible flow of UI interactions. It would be so much nicer to be able to see what are you doing.

Second — What happens when a set of components we are testing, interacts with Browser APIs like cookies, page URL or local storage, which cannot be easily included in Enzyme or React testing library. Testing even the simple action of “user clicked on link powered by react-router and app transitioned to the new route” requires quite a bit of work to replace actual APIs with mocks and test.

Cypress to the rescue

Cypress resolves these problems. It is an Electron application that runs tests in environments of Electron app, Chrome browser or headless.

Simply by having our code in the instance of a browser, we can see our tests visually and get access to all Browser APIs. But Cypress offers more. It gives us a detailed and visual report about what is going on in every test, step by step.

Cypress gives easy programmatical way to interact with Browser APIs. I can clean browser localStorage with `cy.clearLocalStorage( )` or set cookies as part of tests setup or teardown with `cy.setCookie()`.

Cypress is also able to communicate with web servers with `cy.request()` and stub request to a backend with `cy.route()`.

Cypress presents itself as a platform for end to end tests. There are also plugins that can extend Cypress functionality even further and allow us to do more with it. We will discuss only a tiny subset of what Cypress can do — integration tests of UI with all communication to the server stubbed by Cypress. You may think about these tests as “UI in a box”.

Cypress setup

In this section, we will add Cypress into an imaginable app. It is based on the project I currently work on, but with a twist. Who’s interested in discussing an obscure internal dashboard? Most likely, nobody. So instead, our app is called “Michelin-starred recipes”. Users of the app can browse recipes and filter them according to ingredients and cuisine type. There is also an important toggle of “show only vegan recipes”. The whole app is behind Okta because it is available only for chefs in Michelin-starred restaurants. When the restaurant awarded its first star, the chef gains access to the application via Okta. It’s all top secret stuff :). Let’s take a look at how we can test it!

In the root folder of our project, we will run two following commands

$ npm install cypress --save-dev$ npx cypress open

Cypress adds the following structure of folders into the project.

For the sake of convenience, we will add the following piece of configuration into cypress.json file

{
"baseUrl": "http://localhost:3000"
}

The baseUrl depends, on which host and port your application runs. Mine is localhost: 3000. Thanks to this we will be able to use “/recipes” url instead of daunting “http://localhost:3000/recipes” to load the recipes page.

Getting behind Okta authentication in Cypress

Fist challenge presented by “Michelin-starred recipes” is authentication. One of the Cypress limitations (or features) is that every test can visit only one domain level URL.

So I cannot start my test from the login page in my app, go to a third-party-domain (Okta), log in here and be redirected back to my app. The moment I will leave my app page, Cypress will throw an error.

Cypress documentation offers two solutions. First — to mock authenticated state of a user. Second — to login programmatically with `cy.request()`.

We will implement the first option. It is blazing fast — no request is sent and no waiting for a response. Our app uses okta-react package, so the following snippet shows how to mock authentication in this specific case.

Mocked authentication also means, that any request to the server, if it requires user to be authenticaticated, should be mocked too. And Cypress will help and happily stub any requests to the server for us, with `cy.route()` function.

We will obtain everything we need for mocking okta logged-in state by logging into application in a browser, as any user will do, and checking DevTools. We will wrap all we need to do in one function, which will do the following

  • Set two Okta related cookies. We just copy-paste them from our browser.
  • Create a datestamp, which will be one-day in the future, every time the code is run. We name it appropriately “oneDayFromNow”,
  • Set two Okta related items in LocalStorage. They are also copy-pasted from the browser but require some editing. Instead of simple strings, we will use `Template literals`. Inside them, we need to find and replace every instance of expiresAt property, with expiresAt:${oneDayFromNow}
  • Finally, we start a cypress server and say it to a stub a https://dev-382196.okta.com/oauth2/default/v1/userinfo route. We know that this is the route to get user information for our okta user.

Let’s add this code into ./cypress/support/commands.js file

./cypress/support/commands.js

We added a new “Command” — a function, which can be invoked anywhere within Cypress.

Now let's use it. We will add another command, into the ./cypress/support/commands.js file. This one will call mockOktaLoggedState command we defined earlier, stub the API endpoints called by recipes page use and finally visit the “/recipes” page of our app.

./cypress/support/commands.js

Both these commands use fixtures. Fixtures are plain JSON files and in our case, nothing more than saved HTTP response. Let’s take a look at okta-user-mock.json.

./cypress/fixtures/okta-user-mock.json

Close up on one test

Now we will take a look at one test,which checks how recipe’s filtering with Vegan-only toggle works.

Cypress — test passing
./cypress/integration/recipes-page.test.js

The test does the following

  • loads the whole react application and navigate to `/recipes`.
  • sends HTTP request to `/api/recipes’ on page load` to get recipes (mocked response returned by Cypress).
  • assert that not every recipe contain vegan badges
  • toggles the toggle, sends HTTP requests to `/api/recipes?vegan=true` (response returned by Cypress again)
  • asserts that now every recipe contain vegan badges
  • and finally, checks that changes in toggle state reflected in the page URL.

As you can see the list is long, but the actual code snippet is quite concise. This test is also similar to the broken test I mentioned at the beginning of the post. What was broken in that test, is a connection between the state of the toggle and state or the page URL.

Conclusion

Cypress is an exciting tool because it makes testing so much more understandable and far more visual than ever before. In this article, we didn't even scratch the surface of integration tests in Cypress, not to mention all other things it can do. My main motivation was to show how easy it is to start writing tests in Cypress for the application behind Okta authentication. And if it will inspire someone working with React and Okta to give Cypress a try, I couldn’t wish for more.

--

--