Recently a Cypress asked in my Discord channel the following question:
I have a question, is the below a good practice? I have a 'Test' class (in another file, of course), in it there is a method that takes text from an element. Is assigning an alias in the class and then using that alias in the test a correct approach? Because it's a bit unclear in the test, especially when the project grows, where a given alias comes from.
How does the test "know" if a page object method sets a Cypress alias? Let me describe how I would write the same test.
🎁 You can find the source code for this blog post in the repo bahmutov/cypress-alias-example.
The original code
I will start with placing the Test
class in the Cypress E2E support file.
1 | export class Test { |
1 | import { Test } from './test' |
The test passes, even if it unclear from the screenshot what it is checking.
Tip: if all you do inside a cy.then(callback)
is calling another function, you can use point-free or tacit programming style to write less code.
1 | test.getText() // it provides the alias '@buttonText' |
Printing values to the console does not show you the value. Thus I prefer using simple assertions to log the value in the Cypress Log
1 | test.getText() // it provides the alias '@buttonText' |
Nice.
Change the page object
I don't object to Page Objects in general, but I do think they should be much simpler than most people try to make them. For instance (pun intended), the page objects should never be objects with its own internal state. Thus let's refactor Test
to be a simple static object, no need to even use new Test
to call a static method getText
1 | export const Test = { |
1 | import { Test } from './test' |
Works exactly the same. The Test
object is a collection of static methods, even if it creates a global alias side effect @buttonText
.
Change the comment placement
In the original code, the user placed the comment at the call site in the spec file
1 | // cypress/e2e/spec.cy.js |
A better place for this comment would be in the page object itself using a JSDoc method documentation.
1 | export const Test = { |
Any test code calling Test.getText
method can see the documentation snippet.
Do not hard-code the alias
Great. But we still hard-code the alias buttonText
. If we call the same getText
method twice, we will overwrite the previous subject. We can make it better by passing the alias name as an argument.
1 | export const Test = { |
1 | import { Test } from './test' |
The IntelliSense snippet shows the alias parameter.
Tip: you can make your JSDoc snippets better by adding a few examples:
1 | export const Test = { |
I use a lot of JSDoc examples in my tests and utility functions.
Write Page Objects using TypeScript
Even when coding my specs using JavaScript, I like using TypeScript for utilities. So I add TypeScript dev dependency and a tsconfig.json
file
1 | { |
The test.js
file becomes test.ts
with slightly simpler JSDoc comment
1 | export const Test = { |
The JavaScript spec can be checked using the // @ts-check
comment
1 | // @ts-check |
I changed the assertion .should('be.a', 'string')
to a callback to show a problem: cy.get
assumes it yields a jQuery object
We can fix this either by switching the spec to TypeScript and writing cy.get<string>('@myText')
or by adding a page object method to the Test
object.
1 | export const Test = { |
Let's use the Test.getAlias
from the test JS spec
1 | // @ts-check |
IntelliSense popup shows the correct subject for Test.getAlias
TypeScript "knows" the s
argument is a string.
Nice.