Cypress.io — Scaling E2E testing with custom commands
So you’ve been using Cypress.io for long enough to want custom commands. You’ve probably identified some common patterns or want to break up a test into smaller pieces are semantic to your application. A custom command is probably the next logical step.
Command API
The Cypress API can be extended to either overwrite existing commands or add new commands. The Cypress documentation does a pretty good job explaining custom commands and when to define your own. They do explain that some commands can be abstracted into functions. Cypress chains are a compositional pipeline of data.
Below is an example adding a custom command with type definitions:
The Command API is fine if you use JavaScript, but it is more cumbersome in TypeScript. There are a few problems with this approach: you cannot import
or export
modules — TypeScript uses import
and export
to differentiate between namespaces and modules. Since the Cypress API is declared as a namespace (a.k.a. global — your file doesn’t import 'cypress’
), the type definition also has to be a namespace. There are instances where I’ve had to separate the implementation of the command and the type definition to get around this restriction. Also username
, password
and return values are typed multiple times which increases mistakes.
Why not use plain old functions and use composition instead?
Composition
Cypress commands are Promise-like (have a .then
method) and can be composed. A promise chain is like a pipeline for data to be transformed asynchronously from one step to another.
Naming functions allows us to make meaningful abstractions specific to the application that are easy to communicate. Note that this functional composition has a restriction that each function has one input and one output. updateName
is actually a curried function that takes a name
and returns a function. This allows the commands to be chained without specifying parameters (so-called points-free). This style basically reads like a list of steps: “create todo then save todo then update name then mark as done then save todo”.
The same level of application abstraction can be achieved with Cypress. Let’s look at the Cypress Todo MVC example (original found here).
This concept is similar to Page Objects, but we just call them helper functions.
At LogRhythm, we have an in-house component library with many Cypress tests that verify every aspect of user interaction. Helper functions are created for components, verified through CI, and exported for the Application to use to compose richer interactions. For example, selecting an item from a custom drop-down select. Some helpers contain shortcuts (ex: directly manipulating application state), but often interacting with the UI is easier to maintain than shortcuts.
We also use special test attribute selectors meant for our helper functions and not to be used directly. This reduces failures due to UI changes. Helper functions tend to be domain-specific and therefore change with the underlying implementation. As long as the API to the helper function doesn’t change, tests utilizing them still pass even with refactors!
Conclusion
Helper functions increase understanding of test code, decrease time to write new tests and decrease failures. There are a few more ways to make testing more successful including documenting functions and cleaning up Cypress logging using the Cypress log API, but that’s for another article.