Skip to content

Commit

Permalink
Merge pull request #4148 from cypress-io/commands-are-not-promises
Browse files Browse the repository at this point in the history
More clearly explain that cypress commands are not promises
  • Loading branch information
Blue F committed Oct 13, 2021
2 parents f41cd75 + 77fdca4 commit 933cd58
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 147 deletions.
22 changes: 0 additions & 22 deletions content/guides/core-concepts/conditional-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,25 +557,3 @@ on other commands.
If you cannot accurately know the state of your application then no matter what
programming idioms you have available - **you cannot write 100% deterministic
tests**.

Still not convinced?

Not only is this an anti-pattern, but it's an actual logical fallacy.

You may think to yourself... okay fine, but 4 seconds - man that's not enough.
Network requests could be slow, let's bump it up to 1 minute!

Even then, it's still possible a WebSocket message could come in... so 5
minutes!

Even then, not enough, it's possible a `setTimeout` could trigger... 60 minutes.

As you approach infinity your confidence does continue to rise on the chances
you could prove the desired state will be reached, but you can never prove it
will. Instead you could theoretically be waiting for the heat death of the
universe for a condition to come that is only a moment away from happening.
There is no way to prove or disprove that it _may_ conditionally happen.

You, the test writer, must know ahead of time what your application is
programmed to do - or have 100% confidence that the state of a mutable object
(like the DOM) has stabilized in order to write accurate conditional tests.
149 changes: 24 additions & 125 deletions content/guides/core-concepts/introduction-to-cypress.md
Original file line number Diff line number Diff line change
Expand Up @@ -852,102 +852,40 @@ the timeout is reached, the test will fail.

</Alert>

### Commands Are Promises
### The Cypress Command Queue

This is the big secret of Cypress: we've taken our favorite pattern for
composing JavaScript code, Promises, and built them right into the fabric of
Cypress. Above, when we say we're enqueuing actions to be taken later, we could
restate that as "adding Promises to a chain of Promises".
While the API may look similar to Promises, with it's `then()` syntax, Cypress
commands are not promises - they are serial commands passed into a central
queue, to be executed asynchronously at a later date. These commands are
designed to deliver deterministic, repeatable and consistent tests.

Let's compare the prior example to a fictional version of it as raw,
Promise-based code:

#### Noisy Promise demonstration. Not valid code.

```js
it('changes the URL when "awesome" is clicked', () => {
// THIS IS NOT VALID CODE.
// THIS IS JUST FOR DEMONSTRATION.
return cy
.visit('/my/resource/path')
.then(() => {
return cy.get('.awesome-selector')
})
.then(($element) => {
// not analogous
return cy.click($element)
})
.then(() => {
return cy.url()
})
.then((url) => {
expect(url).to.eq('/my/resource/path#awesomeness')
})
})
```

#### How Cypress really looks, Promises wrapped up and hidden from us.

```javascript
it('changes the URL when "awesome" is clicked', () => {
cy.visit('/my/resource/path')

cy.get('.awesome-selector').click()

cy.url().should('include', '/my/resource/path#awesomeness')
})
```

Big difference! In addition to reading much cleaner, Cypress does more than
this, because **Promises themselves have no concepts of
[retry-ability](/guides/core-concepts/retry-ability)**.

Without [**retry-ability**](/guides/core-concepts/retry-ability), assertions
would randomly fail. This would lead to flaky, inconsistent results. This is
also why we cannot use new JS features like `async / await`.

Cypress cannot yield you primitive values isolated away from other commands.
That is because Cypress commands act internally like an asynchronous stream of
data that only resolve after being affected and modified **by other commands**.
This means we cannot yield you discrete values in chunks because we have to know
everything about what you expect before handing off a value.

These design patterns ensure we can create **deterministic**, **repeatable**,
**consistent** tests that are **flake free**.
Almost all commands come with built-in
[retry-ability](/guides/core-concepts/retry-ability)**. Without
[**retry-ability**](/guides/core-concepts/retry-ability), assertions
would randomly fail. This would lead to flaky, inconsistent results.

<Alert type="info">

Cypress is built using Promises that come from
[Bluebird](http://bluebirdjs.com/). However, Cypress commands do not return
these typical Promise instances. Instead we return what's called a `Chainer`
that acts like a layer sitting on top of the internal Promise instances.

For this reason you cannot **ever** return or assign anything useful from
Cypress commands.

If you'd like to learn more about handling asynchronous Cypress Commands please
read our [Core Concept Guide](/guides/core-concepts/variables-and-aliases).
While Cypress is built using Promises that come from
[Bluebird](http://bluebirdjs.com/), these are not what we expose
as commands and assertions on `cy`. If you'd like to learn more about
handling asynchronous Cypress Commands please read our
[Core Concept Guide](/guides/core-concepts/variables-and-aliases).

</Alert>

### Commands Are Not Promises

The Cypress API is not an exact 1:1 implementation of Promises. They have
Promise like qualities and yet there are important differences you should be
aware of.
Commands also have some design choices that developers who are used to promise-based
testing may find unexpected. They are intentional decisions on Cypress' part,
not technical limitations.

1. You cannot **race** or run multiple commands at the same time (in parallel).
2. You cannot 'accidentally' forget to return or chain a command.
3. You cannot add a `.catch` error handler to a failed command.

There are _very_ specific reasons these limitations are built into the Cypress
API.
2. You cannot add a `.catch` error handler to a failed command.

The whole intention of Cypress (and what makes it very different from other
The whole purpose of Cypress (and what makes it very different from other
testing tools) is to create consistent, non-flaky tests that perform identically
from one run to the next. Making this happen isn't free - there are some
trade-offs we make that may initially seem unfamiliar to developers accustomed
to working with Promises.
to working with Promises or other libraries.

Let's take a look at each trade-off in depth:

Expand All @@ -971,43 +909,6 @@ manner in order to create consistency. Because integration and e2e tests
primarily mimic the actions of a real user, Cypress models its command execution
model after a real user working step by step.

#### You cannot accidentally forget to return or chain a command

In real promises it's very easy to 'lose' a nested Promise if you don't return
it or chain it correctly.

Let's imagine the following Node code:

```js
// assuming we've promisified our fs module
return fs.readFile('/foo.txt', 'utf8').then((txt) => {
// oops we forgot to chain / return this Promise
// so it essentially becomes 'lost'.
// this can create bizarre race conditions and
// bugs that are difficult to track down
fs.writeFile('/foo.txt', txt.replace('foo', 'bar'))

return fs.readFile('/bar.json').then((json) => {
// ...
})
})
```

The reason this is even possible to do in the Promise world is because you have
the power to execute multiple asynchronous actions in parallel. Under the hood,
each promise 'chain' returns a promise instance that tracks the relationship
between linked parent and child instances.

Because Cypress enforces commands to run _only_ serially, you do not need to be
concerned with this in Cypress. We enqueue all commands onto a _global_
singleton. Because there is only ever a single command queue instance, it's
impossible for commands to ever be _'lost'_.

You can think of Cypress as "queueing" every command. Eventually they'll get run
and in the exact order they were used, 100% of the time.

There is no need to ever `return` Cypress commands.

#### You cannot add a `.catch` error handler to a failed command

In Cypress there is no built in error recovery from a failed command. A command
Expand All @@ -1020,12 +921,10 @@ You might be wondering:
> does (or doesn't) exist, I choose what to do?
The problem with this question is that this type of conditional control flow
ends up being non-deterministic. This means it's impossible for a script (or
robot), to follow it 100% consistently.

In general, there are only a handful of very specific situations where you _can_
create control flow. Asking to recover from errors is actually the same as
asking for another `if/else` control flow.
ends up being non-deterministic. This means different test runs may behave
differently, which makes them less deterministic and consistent. In general,
there are only a handful of very specific situations where you _can_
create control flow using Cypress commands.

With that said, as long as you are aware of the potential pitfalls with control
flow, it is possible to do this in Cypress!
Expand Down

0 comments on commit 933cd58

Please sign in to comment.