Search⌘ K
AI Features

Sharing Authentication State

Explore how to maintain test independence by managing authentication state in Cypress with custom commands. Learn to store and restore user tokens, ensure stability, and improve testing speed while keeping each test isolated and reliable.

We know that the state must not be shared between subsequent tests because it’s the first reason for brittle tests .

Remember the Testing Rules: why should we not share the state between tests? Imagine this scenario:

  • test a registers a new user and performs actions that require the user to be registered.
  • test b preforms actions that require the user to be registered

You are going to break test b test if

  • You run it alone with it.only().

  • You skip the test a with it.skip().

  • You change the order.

  • You move the test b to another file or vice-versa.

Every test must be independent.

In the previous lesson, we improved test performance, but it’s still quite slow. To maintain independence, we could write a custom command that registers a new user, stores the user token, and restores the token before the next step!

One thing at a time:

  1. The RealWorld front-end stores the user token with a jwt local storage field.

  2. We can copy it after the first registration (it can be accessed in Cypress with localStorage.jwt).

  3. Then, we can restore it for every future test (checking that the user is still valid and authenticated for the back-end).

Let’s write the cy.signupV3 custom command, starting from the cy.signupV2 command that’s as follows:

C++
import { paths } from "../../../realworld/frontend/src/components/App";
import { noArticles } from "../../../realworld/frontend/src/components/ArticleList";
Cypress.Commands.add("signupV2", ({ email, username, password } = {}) => {
const random = Math.floor(Math.random() * 100000);
const user = {
username: username || `Tester${random}`,
email: email || `user+${random}@realworld.io`,
password: password || "mysupersecretpassword"
};
// set up AJAX call interception
cy.server();
cy.route("POST", "**/api/users").as("signup-request");
cy.visit(paths.register);
cy.window()
.its("appActions")
.invoke("signup", user);
// ... and AJAX call waiting
cy.wait("@signup-request").should(xhr => {
expect(xhr.request.body).deep.equal({
user: {
username: user.username,
email: user.email,
password: user.password
}
});
expect(xhr.status).to.equal(200);
cy.wrap(xhr.response.body)
.should("have.property", "user")
.and(
user =>
expect(user)
.to.have.property("token")
.and.to.be.a("string").and.not.to.be.empty
)
.and("deep.include", {
username: user.username.toLowerCase(),
email: user.email
});
});
// end of the flow
cy.findByText(noArticles).should("be.visible");
// restore the original cy.server behavior
cy.server({ enable: false });
cy.then(() => user);
});

Exploring cy.gignupv3

Let’s look at where we would store the token in the following code.

Storing the token

C++
import { paths } from "../../../realworld/frontend/src/components/App";
import { noArticles } from "../../../realworld/frontend/src/components/ArticleList";
+let previousUserData = {};
Cypress.Commands.add("signupV2", ({ email, username, password } = {}) => {
// ... the command code...
cy
+ // wait until the localStorage token is saved
+ .wrap(localStorage)
+ .its("jwt")
+ .should(jwt => expect(jwt).to.be.a("string").and.not.to.be.empty)
+ .then(jwt => {
+ previousUserData = {
+ jwt,
+ username,
+ email,
+ password,
+ user
+ };
+ })
.then(() => user);
});

What the new code does:

  • Waits until the localStorage.jwt token is available. The front-end sets it asynchronously. So, the jwt retrieval (.its("jwt")) is retried using the Cypress inner retry ability until the next assertion, .should(jwt => expect(jwt).to.be.a("string").and.not.to.be.empty), passes.

  • Once the jwt token is set, it stores all the user data (the token, the parameters, and the new user object).

  • “returns” the user object with .then(() => user). This is an asynchronous flow so it “returns” the object by chaining it.

Initial token check

C++
import { paths } from "../../../realworld/frontend/src/components/App";
import { noArticles } from "../../../realworld/frontend/src/components/ArticleList";
let previousUserData = {};
-Cypress.Commands.add("signupV2", ({ email, username, password } = {}) => {
+Cypress.Commands.add("signupV3", ({ email, username, password, ignoreLocalStorage = false } = {}) => {
+ let user;
// if the user data match the previous one
+ if (!ignoreLocalStorage && previousUserData.jwt && previousUserData.email === email) {
+ cy.log("signupV3: Authentication check ⚠️");
// set the stored token
+ localStorage.setItem("jwt", previousUserData.jwt);
+ cy.visit("/")
// use the "New Post" string to detect if the user is authenticated or not
+ cy.findByText(newPost)
+ .then($el => $el.length !== 0)
+ .then(userIsAuthenticated => {
+ if (userIsAuthenticated) {
+ cy.log("signupV3: Authentication check passed ✅");
+ user = previousUserData.user;
+ } else {
+ // removed the stored token
+ localStorage.removeItem("jwt");
+ }
+ });
+ }
+ cy.then(() => {
+ if (user) {
+ return user;
+ }
+ cy.log("signupV3: Authentication check failed ❌ a new user is going to be registered");
// ... the rest of the command code...
});

Step by step:

  1. We need to check that the user and the “previous” user are the same.We could do this by checking the cy.signup() parameters to learn what email the test wants to register. Essentially, We must check that a “previous user” exists.
if (previousUserData.jwt && previousUserData.email === email) {
  // ...
}
  1. The special “user restore” capability must be optional, with a new ignoreLocalStorage option.
if (
  !ignoreLocalStorage &&
  previousUserData.jwt &&
  previousUserData.email === email
) {
  // ...
}
  1. The previous user’s token is set
localStorage.setItem("jwt", previousUserData.jwt);
  1. We must check that the token is still valid for the back-end application. This should not be an issue unless the test takes a long time.Regardless ** test independence and stability must not be put at risk by a missing control **. So, this check must be performed from the user perspective (as we read in the cypress-testing-library lesson).
cy.visit("/")
// use the "New Post" string to detect if the user is authenticated or not
cy.findByText(newPost)
  .then($el => $el.length !== 0)
  .then(userIsAuthenticated => {
    // now you know if the user is authenticated or not
  });
  1. If the user is authenticated that’s ok. If they are not, we must restore the initial front-end state by removing the jwt token
.then(userIsAuthenticated => {
  if (userIsAuthenticated) {
    cy.log("signupV3: Authentication check passed ✅");
    user = previousUserData.user;
  } else {
    // removed the stored token
    localStorage.removeItem("jwt");
  }
  1. We must skip the previous register flow.
if (user) {
  return user;
}

The whole command code is below:

File: cypress/support/signup/signup-v3.js

C++
import { paths } from "../../../realworld/frontend/src/components/App";
import { noArticles } from "../../../realworld/frontend/src/components/ArticleList";
import { newPost } from "../../../realworld/frontend/src/components/Header";
let previousUserData = {};
Cypress.Commands.add(
"signupV3",
({ email, username, password, ignoreLocalStorage = false } = {}) => {
let user;
// if the user data match the previous one
if (!ignoreLocalStorage && previousUserData.jwt && previousUserData.email === email) {
cy.log("signupV3: Authentication check ⚠️");
// set the stored token
localStorage.setItem("jwt", previousUserData.jwt);
cy.visit("/")
// use the "New Post" string to detect if the user is authenticated or not
.findByText(newPost)
.then($el => $el.length !== 0)
.then(userIsAuthenticated => {
if (userIsAuthenticated) {
cy.log("signupV3: Authentication check passed ✅");
user = previousUserData.user;
} else {
// removed the stored token
localStorage.removeItem("jwt");
}
});
}
cy.then(() => {
if (user) {
return user;
}
cy.log("signupV3: Authentication check failed ❌ a new user is going to be registered");
const random = Math.floor(Math.random() * 100000);
user = {
username: username || `Tester${random}`,
email: email || `user+${random}@realworld.io`,
password: password || "mysupersecretpassword"
};
// set up AJAX call interception
cy.server();
cy.route("POST", "**/api/users").as("signup-request");
cy.visit(paths.register);
cy.window()
.its("appActions")
.invoke("signup", user);
// ... and AJAX call waiting
cy.wait("@signup-request").should(xhr => {
expect(xhr.request.body).deep.equal({
user: {
username: user.username,
email: user.email,
password: user.password
}
});
expect(xhr.status).to.equal(200);
cy.wrap(xhr.response.body)
.should("have.property", "user")
.and(
user =>
expect(user)
.to.have.property("token")
.and.to.be.a("string").and.not.to.be.empty
)
.and("deep.include", {
username: user.username.toLowerCase(),
email: user.email
});
});
// end of the flow
cy.findByText(noArticles).should("be.visible");
// restore the original cy.server behavior
cy.server({ enable: false });
// wait until the localStorage token is saved
cy.wrap(localStorage)
.its("jwt")
.should(jwt => expect(jwt).to.be.a("string").and.not.to.be.empty)
.then(jwt => {
previousUserData = {
jwt,
username,
email,
password,
user
};
})
.then(() => user);
});
}
);

Remember that while you can implement shared utilities or the above flow any way you want to, you should always consider test independence and test stability

With the new cy.signupV3 custom command we know that:

  • The first user is registered as usual.

  • If it’s possible, the test takes advantage of the previously registered user.

  • If something goes wrong and the back-end does not recognize the previous user, a new one is registered.

Every test that leverages the cy.signupV3 command gets a registered user, the fastest way possible.

Let’s have a look at the following test which will leverage the signupV3 command.

Note: You can see the Cypress UI better by opening the link next to Your app can be found at:

import { newPost } from "/educative-cypress-course/realworld/frontend/src/components/Header";

context("The custom command could be run before the test code", () => {
  it("Should leverage the custom registration command", () => {
    cy.signupV3().should(user => {
      expect(user).to.have.property("username").and.not.to.be.empty;
      expect(user).to.have.property("email").and.not.to.be.empty;
      expect(user).to.have.property("password").and.not.to.be.empty;
    });

    cy.log("The user is now registered and authenticated");
    cy.findByText(newPost).should("be.visible");
  });
});

context("The custom command could be run before the test code with a test hook", () => {
  beforeEach(() => {
    cy.signupV3().should(user => {
      expect(user).to.have.property("username").and.not.to.be.empty;
      expect(user).to.have.property("email").and.not.to.be.empty;
      expect(user).to.have.property("password").and.not.to.be.empty;
    });
  });
  it("Should leverage the custom registration command with a test hook", () => {
    cy.log("The user is now registered and authenticated");
    cy.findByText(newPost).should("be.visible");
  });
});

context("The custom command could be customized", () => {
  it("Should leverage the custom registration command", () => {
    const user = {
      username: "CustomTester",
      email: "specialtester@realworld.io",
      password: "mysupersecretpassword"
    };
    cy.signupV3(user).should("deep.equal", user);
    cy.log("The user is now registered and authenticated");
    cy.findByText(newPost).should("be.visible");
  });
});
Test

Note: The performance improvements vary for every application. We artificially slowed down the RealWorld app for this course.