Custom command
The same way we have created a custom signup command for the E2E tests, we could create an authentication command for the UI Integration Tests too. It will be way far simpler because we do not need a real signup/authentication, obviously: we just need that the front-end thinks that's authenticated.
What we have to do is essentially setting the jwt
token into the local storage. Let's write the authenticateIntegration
custom command.
Where we can get a valid jwt
? Well, from the signup.json
fixture!
File: cypress/fixtures/users/signup.json
{
"user": {
"username": "tester",
"email": "user@realworld.io",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkN2ZiNmVhZDkzZWQ5MDhiMGU3MDMzYiIsInVzZXJuYW1lIjoidGVzdGVyIiwiZXhwIjoxNTczODM4NTg3LCJpYXQiOjE1Njg2NTA5ODd9.jeAccqZi6dOqokwjRPFl4fzHE5s5p8sB32NgXwlgrxQ"
}
}
Cypress allows us to read the fixtures using the cy.fixture
command. All we need to do is:
reading the fixture
reading the contained token
setting the
jwt
token
The command code is the following
File: cypress/support/authentication/authenticate-integration.js
/// <reference types="Cypress" />
Cypress.Commands.add("authenticateIntegration", () => {
cy.fixture("users/signup")
.its("user")
.should(
user =>
expect(user)
.to.have.property("token")
.and.to.be.a("string").and.not.to.be.empty
)
.then(user => localStorage.setItem("jwt", user.token));
});
Please note that there are some assertions about the fixture content itself, the goal is always the same: getting the more out of the assertion feedback. If we change the fixture content accidentally the command is going to prompts us with a useful message.
The test that navigates the home page leveraging an already "authenticated" is the following
context("The custom command could be run before the test code", () => {
it("Should leverage the custom authentication command", () => {
cy.authenticateIntegration().should(user => {
expect(user).to.have.property("username").and.not.to.be.empty;
expect(user).to.have.property("email").and.not.to.be.empty;
});
cy.intercept("GET", "**/api/tags", { fixture: "tags/empty-tags" }).as("get-tags");
cy.intercept(
"GET",
"**/api/articles/feed**",
{ fixture: "articles/empty-articles" }
).as("get-feed");
cy.visit("/");
cy.wait(["@get-tags", "@get-feed"]);
cy.findByText(newPost).should("be.visible");
});
});
But, when running it, we get an error from Cypress
the Test Runner speaks for itself: the RealWorld front-end fires a GET api/user
expecting the same data returned by the signup AJAX request (POST api/users
), nothing that we cannot stub and wait in a while
+cy.intercept("GET", "**/api/user", { fixture: "users/signup" }).as("get-user");
cy.intercept("GET", "**/api/tags", { fixture: "tags/empty-tags" }).as("get-tags");
cy.intercept("GET", "**/api/articles/feed**", { fixture: "articles/empty-articles" }).as("get-feed");
-cy.wait(["@get-tags", "@get-feed"]);
+cy.wait(["@get-user", "@get-tags", "@get-feed"]);
Here the full code of the test
File: cypress/integration/examples/authenticate-command/authenticate-command-1.integration.spec.js
/// <reference types="Cypress" />
import { newPost } from "../../../../realworld/frontend/src/components/Header";
const headers = { "Access-Control-Allow-Origin": "*" }
context("The custom command could be run before the test code", () => {
it("Should leverage the custom authentication command", () => {
cy.authenticateIntegration().should((user) => {
expect(user).to.have.property("username").and.not.to.be.empty;
expect(user).to.have.property("email").and.not.to.be.empty;
});
cy.intercept("GET", "/api/user", { fixture: "users/signup.json", headers }).as("get-user");
cy.intercept("GET", "**/api/tags", { fixture: "tags/empty-tags.json", headers }).as("get-tags");
cy.intercept("GET", "**/api/articles/feed**", {fixture: "articles/empty-articles", headers }).as("get-feed");
cy.visit("/");
cy.wait(["@get-user", "@get-tags", "@get-feed"]);
cy.findByText(newPost).should("be.visible");
});
});
An all-in-one command
Since the front-end does a call to the GET api/user
API as its very first thing, we could write a more complete command that cares about the request itself. We only have to move part of the code of the authenticate-command-1.integration.spec.js test to the command code.
+import { newPost } from "../../../realworld/frontend/src/components/Header";
-Cypress.Commands.add("authenticateIntegration", () => {
+Cypress.Commands.add("authenticateAndVisitIntegration", path => {
+ cy.intercept("GET", "**/api/user", {
+ fixture: "users/signup",
+ headers: { "Access-Control-Allow-Origin": "*" }
+ }).as("get-user");
cy.fixture("users/signup")
.its("user")
.should(
user =>
expect(user)
.to.have.property("token")
.and.to.be.a("string").and.not.to.be.empty
)
.then(user => localStorage.setItem("jwt", user.token));
+ cy.visit(path);
+ cy.wait("@get-user");
+ cy.findByText(newPost).should("be.visible");
});
The new authenticateAndVisitIntegration
command is way more complete and the previous "visit the home page" test could be simplified
it("Should leverage the custom authentication command", () => {
- cy.authenticateIntegration().should(user => {
- expect(user).to.have.property("username").and.not.to.be.empty;
- expect(user).to.have.property("email").and.not.to.be.empty;
- });
- cy.intercept("GET", "/api/user", { fixture: "users/signup.json", headers }).as("get-user");
cy.intercept("GET", "**/api/tags", { fixture: "tags/empty-tags", headers }).as("tags");
cy.intercept("GET", "**/api/articles/feed**", { fixture: "articles/empty-articles", headers }).as("feed");
- cy.visit("/");
+ cy.authenticateAndVisitIntegration("/");
- cy.wait(["@get-user", "@get-tags", "@get-feed"]);
+ cy.wait(["@get-tags", "@get-feed"]);
- cy.findByText(newPost).should("be.visible");
});
and a generic "visit and check" test is something like this:
it("Should leverage the custom authentication command to navigate the editor page", () => {
cy.authenticateAndVisitIntegration("/editor");
// the rest of the code
});
The complete code is the following
File: cypress/integration/examples/authenticate-command/authenticate-command-2.integration.spec.js
/// <reference types="Cypress" />
context("The custom command could be run before the test code", () => {
it("Should leverage the custom authentication command to navigate the home page", () => {
cy.intercept("GET", "**/api/tags", { fixture: "tags/empty-tags" }).as("get-tags");
cy.intercept("GET", "**/api/articles/feed**", { fixture: "articles/empty-articles" }).as("get-feed");
cy.authenticateAndVisitIntegration("/");
cy.wait(["@get-tags", "@get-feed"]);
});
it("Should leverage the custom authentication command to navigate the editor page", () => {
cy.authenticateAndVisitIntegration("/editor");
// the rest of the code
});
});
Take a look at the duration of the second test
it takes just three tenths of a second to have everything set up as an authenticated user, before starting to test its own flow. Just three tenths of a second! This is what we mean when we talk about the importance of fast tests!
Author: Stefano Magni