Custom commands
At least half of the RealWorld features require the user to be authenticated. By default, Cypress forces you to not share the state between the tests and it makes sense because it's a huge anti-pattern. We need to transform the signup process into something usable by every test: a Cypress custom command.
A Cypress Custom Command is a function that can be called from the global cy
object. We are going to create a cy.signupV1()
command to be run before every other test.
The steps needed to create a new Cypress custom command are the next:
create a new file containing the code of the command itself into the
cypress/support
directory. The file is thecypress/support/signup/signup-v1.js
add the wrapper of the code
Cypress.Commands.add("signupV1", (/* the params of the command */) => {
/* the code of the command */
});
- import the newly created file from the
cypress/support/commands.js
file
import "./signup/signup-v1";
- start writing a new test that calls the
cy.signupV1()
command
context("Whatever test", () => {
it("Should leverage the custom registration command", () => {
cy.signupV1();
});
});
- run the test!
Obviously, the new command does nothing at the moment, but this is the foundation of cy.signupV1
command.
First signup command
The first implementation is simple and not very smart, the core of the command is the same of the signup-8-simpler-assertions.e2e.spec.js test.
While creating reusable commands, the testing rules remain the same. The most important ones for our exercise are:
the commands must be as simple as possible
the commands must be flexible and easily customizable
The former is respected because we are going to copy all the code from the just written signup-8-simpler-assertions.e2e.spec.js test. The latter leads us to ask a question: what could be changed with a global and reusable signup utility? Essentially, only the user data.
The command must accept the user data with the usual defaults if not provided
Cypress.Commands.add("signupV1", ({ 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"
};
// The rest of the code...
});
The rest of the code is 100% the same of the signup-8-simpler-assertions.e2e.spec.js test, the only changes are placed in the end:
a
cy.server({ enable: false });
to restore the originalcy.server
behaviora closing
cy.then(() => user);
so the data used to register the user can be consumed by chaining a command to thecy.signupV1()
call
The whole code is the following:
File: cypress/support/signup/signup-v1.js
/// <reference types="Cypress" />
import { paths } from "../../../realworld/frontend/src/components/App";
import { noArticles } from "../../../realworld/frontend/src/components/ArticleList";
import { strings } from "../../../realworld/frontend/src/components/Register";
Cypress.Commands.add("signupV1", ({ 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);
// form filling
cy.findByPlaceholderText(strings.username).type(user.username);
cy.findByPlaceholderText(strings.email).type(user.email);
cy.findByPlaceholderText(strings.password).type(user.password);
// form submit...
cy.get("form")
.within(() => cy.findByText(strings.signUp))
.click();
// ... 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);
});
and the test that leverages the custom command is:
context("Whatever test", () => {
it("Should leverage the custom registration command", () => {
cy.signupV1();
cy.log("The user is now registered and authanticated");
cy.findByText("New Post").should("be.visible");
});
});
Please note that the new command is not a replacement for the signup tests. It allows other tests to leverage the same result but, even if they share (at the moment) almost 99% of the code, they are separated.
The cy.signupV1()
command could be consumed:
calling it before your code
calling it from a
before
/beforeEach
test hook, a function that is called before all the tests (before
) or before every test (beforeEach
)
and:
with the default user data creation:
cy.signupV1()
with custom data:
cy.signupV1({email: "foo@bar.io", username: "foo", password: "bar"})
You can find all the combinations of them into a dedicated test 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");
});
});
The assertions about the user
subject yielded by the custom command are just to demonstrate you what the last cy.then(() => user)
command is useful for: to know the user data from the caling test.
Cypress Custom commands have some useful options that we are not going to cover in this course, take a look at the official documentation for them.
Author: Stefano Magni