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'll cover the following...
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 aregisters a new user and performs actions that require the user to be registered.test bpreforms 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 awithit.skip(). -
You change the order.
-
You move the
test bto 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:
-
The RealWorld front-end stores the user token with a
jwtlocal storage field. -
We can copy it after the first registration (it can be accessed in Cypress with
localStorage.jwt). -
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:
Exploring cy.gignupv3
Let’s look at where we would store the token in the following code.
Storing the token
What the new code does:
-
Waits until the
localStorage.jwttoken is available. The front-end sets it asynchronously. So, thejwt 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
jwttoken is set, it stores all the user data (the token, the parameters, and the newuserobject). -
“returns” the
userobject with.then(() => user). This is an asynchronous flow so it “returns” the object by chaining it.
Initial token check
Step by step:
- 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) {
// ...
}
- The special “user restore” capability must be optional, with a new
ignoreLocalStorageoption.
if (
!ignoreLocalStorage &&
previousUserData.jwt &&
previousUserData.email === email
) {
// ...
}
- The previous user’s token is set
localStorage.setItem("jwt", previousUserData.jwt);
- 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
});
- If the user is authenticated that’s ok. If they are not, we must restore the initial front-end state by removing the
jwttoken
.then(userIsAuthenticated => {
if (userIsAuthenticated) {
cy.log("signupV3: Authentication check passed ✅");
user = previousUserData.user;
} else {
// removed the stored token
localStorage.removeItem("jwt");
}
- We must skip the previous register flow.
if (user) {
return user;
}
The whole command code is below:
File: cypress/support/signup/signup-v3.js
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");
});
});Note: The performance improvements vary for every application. We artificially slowed down the RealWorld app for this course.