Skip to content

Commit

Permalink
break best practices into subpages
Browse files Browse the repository at this point in the history
  • Loading branch information
jaffrepaul committed Apr 12, 2023
1 parent d8d32b1 commit 55956c6
Show file tree
Hide file tree
Showing 14 changed files with 1,136 additions and 1,112 deletions.
1,112 changes: 0 additions & 1,112 deletions docs/guides/references/best-practices.mdx

This file was deleted.

6 changes: 6 additions & 0 deletions docs/guides/references/best-practices/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"label": "Best Practices",
"position": 60,
"collapsible": true,
"collapsed": true
}
61 changes: 61 additions & 0 deletions docs/guides/references/best-practices/assigning-return-values.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
title: Assigning Return Values
sidebar_position: 10
---

:::danger

&#8239;<Icon name="exclamation-triangle" color="red" /> **Anti-Pattern:** Trying
to assign the return value of Commands with `const`, `let`, or `var`.

:::

:::tip

&#8239;<Icon name="check-circle" color="green" /> **Best Practice:** Use
[aliases and closures to access and store](/guides/core-concepts/variables-and-aliases)
what Commands yield you.

:::

Many first time users look at Cypress code and think it runs synchronously.

We see new users commonly write code that looks like this:

```js
// DONT DO THIS. IT DOES NOT WORK
// THE WAY YOU THINK IT DOES.
const a = cy.get('a')

cy.visit('https://example.cypress.io')

// nope, fails
a.first().click()

// Instead, do this.
cy.get('a').as('links')
cy.get('@links').first().click()
```

:::info

<strong>Did you know?</strong>

You rarely have to ever use `const`, `let`, or `var` in Cypress. If you're using
them, you will want to do some refactoring.

:::

If you are new to Cypress and wanting to better understand how Commands work -
[please read our Introduction to Cypress guide](/guides/core-concepts/introduction-to-cypress#Chains-of-Commands).

If you're familiar with Cypress commands already, but find yourself using
`const`, `let`, or `var` then you're typically trying to do one of two things:

- You're trying to **store and compare** values such as **text**, **classes**,
**attributes**.
- You're trying to share **values** between tests and hooks like `before` and
`beforeEach`.

For working with either of these patterns, please read our
[Variables and Aliases guide](/guides/core-concepts/variables-and-aliases).
205 changes: 205 additions & 0 deletions docs/guides/references/best-practices/before-and-after-hooks.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
---
title: Using after Or afterEach Hooks
sidebar_label: Avoid after or afterEach Hooks
sidebar_position: 20
---

:::danger

&#8239;<Icon name="exclamation-triangle" color="red" /> **Anti-Pattern:** Using
`after` or `afterEach` hooks to clean up state.

:::

:::tip

&#8239;<Icon name="check-circle" color="green" /> **Best Practice:** Clean up
state **before** tests run.

:::

We see many of our users adding code to an `after` or `afterEach` hook in order
to clean up the state generated by the current test(s).

We most often see test code that looks like this:

```js
describe('logged in user', () => {
beforeEach(() => {
cy.login()
})

afterEach(() => {
cy.logout()
})

it('tests', ...)
it('more', ...)
it('things', ...)
})
```

Let's look at why this is not really necessary.

### Dangling state is your friend:

One of the **best** parts of Cypress is its emphasis on debuggability. Unlike
other testing tools - when your tests end - you are left with your working
application at the exact point where your test finished.

This is an **excellent** opportunity for you to **use** your application in the
state the tests finished! This enables you to write **partial tests** that drive
your application step by step, writing your test and application code at the
same time.

We have built Cypress to support this use case. In fact, Cypress **does not**
clean up its own internal state when the test ends. We **want** you to have
dangling state at the end of the test! Things like [stubs](/api/commands/stub),
[spies](/api/commands/spy), even [intercepts](/api/commands/intercept) are
**not** removed at the end of the test. This means your application will behave
identically while it is running Cypress commands or when you manually work with
it after a test ends.

If you remove your application's state after each test, then you instantly lose
the ability to use your application in this mode. Logging out at the end would
always leave you with the same login page at the end of the test. In order to
debug your application or write a partial test, you would always be left
commenting out your custom `cy.logout()` command.

### It's all downside with no upside:

For the moment, let's assume that for some reason your application desperately
**needs** that last bit of `after` or `afterEach` code to run. Let's assume that
if that code is not run - all is lost.

That is fine - but even if this is the case, it should not go in an `after` or
`afterEach` hook. Why? So far we have been talking about logging out, but let's
use a different example. Let's use the pattern of needing to reset your
database.

**The idea goes like this:**

> After each test I want to ensure the database is reset back to 0 records so
> when the next test runs, it is run with a clean state.
**With that in mind you write something like this:**

```js
afterEach(() => {
cy.resetDb()
})
```

Here is the problem: **there is no guarantee that this code will run.**

If, hypothetically, you have written this command because it **has** to run
before the next test does, then the absolute **worst place** to put it is in an
`after` or `afterEach` hook.

Why? Because if you refresh Cypress in the middle of the test - you will have
built up partial state in the database, and your custom `cy.resetDb()` function
**will never get called**.

If this state cleanup is **truly** required, then the next test will instantly
fail. Why? Because resetting the state never happened when you refreshed
Cypress.

### State reset should go before each test:

The simplest solution here is to move your reset code to **before** the test
runs.

Code put in a `before` or `beforeEach` hook will **always** run prior to the
test - even if you refreshed Cypress in the middle of an existing one!

This is also a great opportunity to use
[root level hooks in mocha](https://github.com/mochajs/mochajs.github.io/blob/master/index.md#root-level-hooks).

<SupportFileConfiguration />

**Hooks you add to the root will always run on all suites!**

```js
// cypress/support/e2e.js or cypress/support/component.js

beforeEach(() => {
// now this runs prior to every test
// across all files no matter what
cy.resetDb()
})
```

### Is resetting the state necessary?

One final question you should ask yourself is - is resetting the state even
necessary? Remember, Cypress already automatically enforces
[test isolation](/guides/core-concepts/writing-and-organizing-tests#Test-Isolation)
by clearing state before each test. Make sure you are not trying to clean up
state that is already cleaned up by Cypress automatically.

If the state you are trying to clean lives on the server - by all means, clean
that state. You will need to run these types of routines! But if the state is
related to your application currently under test - you likely do not even need
to clear it.

The only times you **ever** need to clean up state, is if the operations that
one test runs affects another test downstream. In only those cases do you need
state cleanup.

#### <Icon name="graduation-cap" /> Real World Example

The <Icon name="github" inline="true" contentType="rwa" /> resets and re-seeds
its database via a custom [Cypress task](/api/commands/task) called `db:seed` in
a `beforeEach` hook. This allows each test to start from a clean slate and a
deterministic state. For example:

```ts
// cypress/tests/ui/auth.cy.ts

beforeEach(function () {
cy.task('db:seed')
// ...
})
```

:::note

_<Icon name="github" /> Source:
[cypress/tests/ui/auth.cy.ts](https://github.com/cypress-io/cypress-realworld-app/blob/develop/cypress/tests/ui/auth.spec.ts)_

:::

The `db:seed` task is defined within the
[setupNodeEvents](/guides/tooling/plugins-guide#Using-a-plugin) function of the
project, and in this case sends a request to a dedicated back end API of the app
to appropriately re-seed the database.

:::cypress-config-plugin-example

```ts
on('task', {
async 'db:seed'() {
// Send request to backend API to re-seed database with test data
const { data } = await axios.post(`${testDataApiEndpoint}/seed`)
return data
},
//...
})
```

:::

:::note

_<Icon name="github" /> Source:
[cypress/plugins/index.ts](https://github.com/cypress-io/cypress-realworld-app/blob/develop/cypress/plugins/index.ts)_

:::

The same practice above can be used for any type of database (PostgreSQL,
MongoDB, etc.). In this example, a request is sent to a back end API, but you
could also interact directly with your database with direct queries, custom
libraries, etc. If you already have non-JavaScript methods of handling or
interacting with your database, you can use [`cy.exec`](/api/commands/exec),
instead of [`cy.task`](/api/commands/task), to execute any system command or
script.
26 changes: 26 additions & 0 deletions docs/guides/references/best-practices/best-practices.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: Best Practices
---

:::info

### <Icon name="graduation-cap" /> Real World Practices

The Cypress team maintains the
&nbsp;<Icon name="github" inline="true" contentType="rwa" />, a full stack
example application that demonstrates **best practices and scalable strategies
with Cypress in practical and realistic scenarios**.

The RWA achieves full [code-coverage](/guides/tooling/code-coverage) with
end-to-end tests
[across multiple browsers](/guides/guides/cross-browser-testing) and
[device sizes](/api/commands/viewport), but also includes
[visual regression tests](/guides/tooling/visual-testing), API tests, unit
tests, and runs them all in an
[efficient CI pipeline](https://cloud.cypress.io/projects/7s5okt).

The app is bundled with everything you need,
[just clone the repository](https://github.com/cypress-io/cypress-realworld-app)
and start testing.

:::
Loading

0 comments on commit 55956c6

Please sign in to comment.