App Actions

We have just written our first Custom Command that registers a new user for every test. What is its biggest problem? Well, it's really, really, slow. The signup-command-1.e2e.spec.js tests take twelve to fifteen seconds to run the empty tests:

Autocompletion


The custom command has all the faults because the tests themselves do nothing, take a look at the test code to double-check it

File: cypress/integration/examples/signup-command/signup-command-1.e2e.spec.js

/// <reference types="Cypress" />

context("The custom command could be run before the test code", () => {
  it("Should leverage the custom registration command", () => {
    cy.signupV1().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("New Post").should("be.visible");
  });
});

context("The custom command could be run before the test code with a test hook", () => {
  beforeEach(() => {
    cy.signupV1().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("New Post").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.signupV1(user).should("deep.equal", user);
    cy.log("The user is now registered and authenticated");
    cy.findByText("New Post").should("be.visible");
  });
});

How the duration could be improved? Let's introduce the concept of App Actions. The same way we leveraged and talked about App Constants, App Actions are utilities exposed directly from the front-end application. Let's work on a signup App Action.

In the RealWorld app front-end app, the registration is managed by the Register.js component. In its constructor, you can find the following code

this.submitForm = (username, email, password) => ev => {
  ev.preventDefault();
  this.props.onSubmit(username, email, password);
};

this.submitForm is the handler that we should trigger automatically from our custom command. We could do that exposing it globally

this.submitForm = (username, email, password) => ev => {
  ev.preventDefault();
  this.props.onSubmit(username, email, password);
}
+window.appActions = window.appActions || {};
+window.appActions.signup = ({username, email, password}) => this.props.onSubmit(username, email, password);

but since polluting the global scope is not ideal and since we need it only when the application is under test, we could check the existence of the window.Cypress object before exposing it

this.submitForm = (username, email, password) => ev => {
  ev.preventDefault();
  this.props.onSubmit(username, email, password);
}

+if(window.Cypress) {
  window.appActions = window.appActions || {};
  window.appActions.signup = ({username, email, password}) => this.props.onSubmit(username, email, password);
+}

If you think that adding fictitious code to the application is not a good idea, remember that:

  • a lot of times the source code is soiled with temporary code/conditions that allow you to manually test some particular conditions (network errors or the authenticated user)

  • you can easily remove the code leveraging Webpack replacing every window.Cypress occurrence with false and then removing the unused code the same way

  • test duration is more important because you are going to run the tests thousands of times

What's the final goal of the window.appActions.signup app action? Simply, calling it from the Cypress test! We can now call it directly deleting the slow form filling. Starting from the code of the signup-v1.js test, the changes are the following

-cy.findByPlaceholderText(strings.username).type(user.username);
-cy.findByPlaceholderText(strings.email).type(user.email);
-cy.findByPlaceholderText(strings.password).type(user.password);
-cy.get("form")
- .within(() => cy.findByText(strings.signUp))
- .click();
+cy.window()
+ .its("appActions")
+ .invoke("signup", user);

You can find the whole code of the custom command into the signup-v2.js custom command

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

/// <reference types="Cypress" />

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);
});

The signupV2 custom command is used by the signup-command-2.e2e.spec.js test and the performance improvement is notable compared to the previous signup-command-1.e2e.spec.js one

Autocompletion


As you can see, there are no interactions with the form itself

that's how App Actions work, they're just test-related shortcuts to perform fast and safe operations.

Fast because:

  • reaching the desired front-end state through the UI is a big and slow anti-pattern. If you take a look at the signup-command-1.e2e.spec.js test, form filling (performed by the signup-v1.js custom command) takes circa 1.5 seconds each time. We can not waste so much time for every test, what could happen when the test suite will count tens or hundreds of tests?

Safe because:

  • we can unleash our imagination when we speak about testing performance improvements. We could apply every kind of esoteric solution but the truth is that a lot of them make the test harder to comprehend, to modify and less aligned with the front-end logic. App actions are clear and concise

  • there are no differences between the front-end code and test code, so there are not two different codes to be maintained

App Actions sometimes can not be used but the more we leverage them, the more robust and fast the tests are.

PageObject

Just for your information: PageObject is the common pattern used widely before the concept of App Actions borned. The problem with the PageObject pattern is that you create one more abstraction to manage that could lose its effectiveness in a while causing more problems than advantages. If you want to deepen the comparison between the two approaches you could read the Stop using Page Objects and Start using App Actions post.

Author: Stefano Magni

results matching ""

    No results matching ""