Recently a user posted in the Cypress Gitter channel the following image and asked why this code is not working.
The above code has several problems. In this blog post, I will fix them all. I have recorded a short video showing the fixes step by step, you can watch the video below, or keep reading.
The returned value
First, let's deal with the returned value of the cy.wait(...).then(...)
chain.
Think about JavaScript promises. A promise .then
method does NOT return the value, it returns another promise, so you can attach another .then
callback, or a callback to catch an error using .catch
, right?
1 | // INCORRECT, the "x" is a promise instance, not the value 42 |
If you want to get the resolved value x
, you need to put the code that uses x
into the last callback.
1 | // FIXED, the "x" is used inside `.then(...)` callback |
Cypress command chains might look like promise chains, but they are more like reactive streams, see this presentation from ReactiveConf 2018. Thus every command like cy.wait
and cy.then
returns another instance of Cypress chainable interface so you can attach more commands. If you want to use the value returned (or "yielded" as Cypress calls it), use it inside .then(...)
callback.
1 | cy.wait('@createUnion').then(response => { |
Printing the value
Let's look at why the console.log(orgId)
prints a weird object.
As I explained above, the returned value of the Cypress callback is an internal chainable object used to add more commands to be executed. The Cypress commands themselves are queued up, the have not started running. The Cypress chains of commands are lazy - they only start running once the browser is ready (which is another difference from the Promises which are very very eager to run). If we look at the order of execution, the console.log
runs way before the Cypress command gets the intercepted response and gets the ID property.
I marked the order of statements executed in the code snippet.
- First, the code runs
cy.wait
to schedule the "WAIT" command. It returns Cypress chainable object - The Cypress chainable object has
then
method, it gets called with a function callback reference. The Cypress method schedules the "THEN" command to be run after "WAIT" is finished (nothing is running at this point). - There are no more Cypress commands to call, thus the assignment
const orgId =
runs, assigning the (unexpected) Cypress chainable object reference to the local variableorgId
- The problem happens here: the next JavaScript statement that runs is the
console.log(orgId)
which tries to print the Chainable object, while the user expects to see the organization id. So it prints something weird. - Cypress test starts running, finds the "WAIT" command, waits for that alias "@createUnion", yields the intercept to the next scheduled command "THEN", calls the function callback which returns the real organization ID.
To fix the code snippet, move the console.log
into the .then
callback that gets the ID, or attach it as another .then
callback.
1 | cy.wait('@createUnion').then(response => { |
Tip: you can print the value to the DevTools using .then(console.log)
but a better idea is to print it to the Cypress Command Log with .then(cy.log)
. See the video How to use Cypress cy.log command to output messages to the Command Log.
In general, anything you get from the application page, or from another Cypress command must be used inside the .then
callback to have its value set. I have two short videos explaining the above problem and how to write your tests correctly.
Why cy.log prints null or undefined
When To Use Cypress .Then Callback To Use The Value
Fluent programming
Now let's refactor the body of the .then(...)
callback function. Currently it takes the response
object from the intercept, runs multiple assertions against it, then yields the organization ID. Let's refactor it for clarity.
🎓 You can find the code from this section in my workshop "Cypress Basics" in the repo bahmutov/cypress-workshop-basics.
Our first attempt mimics the user's question.
1 | it('validates and processes the intercept object', () => { |
The test is green.
Let's look at the code. First, we are only interested in the property response
from the intercept (there are a lot more!). Thus let's extract it using cy.its command.
1 | it('extracts the response property first', () => { |
It runs the same way. If you need to debug the intercept object, click on the "ITS" command and see it in the DevTools console.
Let's look at our assertions. Right now they offer very little to the developer aside from printing their value. Let's add a message to each assertion to make it clearer.
1 | .then((response) => { |
Isn't the top assertion more informative than the next two? We can make it even better by using the specific Chai assertion for checking the property.
1 | .then((response) => { |
If Cypress .then command returns undefined and has no other Cypress commands, then its original subject value gets passed to the next command automatically. Thus we can move the statusCode
check into its own .then
callback to separate it from the response object checks.
1 | cy.wait('@postTodo') |
The output looks the same as before, but now we can notice that we only deal with the body
property from the response
object. Let's apply cy.its
command, just like we extracted the response
from the intercept
object.
1 | cy.wait('@postTodo') |
Remember - the cy.its
command automatically fails if the property does not exist on the object. It also accepts nested properties, so you could grab the body
from the intercept
object using cy.wait(...).its('response.body')
syntax.
Now the last callback only deals with the properties of a single body
object. We can confirm some of the properties using deep.include
assertion.
1 | cy.wait('@postTodo') |
We cannot use deep.equals
since we do not know the "id" property. For more assertion examples, see my Assertions page.
Ok, so returning body.id
at the end could use cy.its
command, so let's move it out. Since we are not returning anything from the .then(body => ...)
callback, the body
wil be yielded to the next command.
1 | cy.wait('@postTodo') |
We now have just the assertions inside .then
callback. Thus we can use the BDD should
assertion instead.
1 | cy.wait('@postTodo') |
I hope the above test code transformation has shown some of the beauty and power of the Cypress fluent syntax where we can chain the commands and the assertions.
Aside: we cannot use the BDD should assertion to verify the status code property like should('have.property', 'statusCode', 201)
fluent syntax because have.property
is one of just a few assertions that change the subject to the property value, while we need to keep the response
object.
Bonus: cy-spok
The test looks good, but there is one improvement we can make. I like the Command Log to be as useful as possible. The standard Chai assertions are good, but cy-spok makes them perfect.
1 | import spok from 'cy-spok' |
Look at the Command Log output - isn't this super helpful?
The plugin cy-spok
is built on top of spok which is really good at asserting nested objects and even checking built-in predicates. For example, we do not know the id
of the item, but we know it is a string. Let's write a single assertion to verify the properties we can inside the entire intercept object.
1 | import spok from 'cy-spok' |
Love it.