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:
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 withfalse
and then removing the unused code the same waytest 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
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