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

Unstubber GET api/users


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

Integration Testing, custom authenticate command


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

results matching ""

    No results matching ""