Skip to content

Commit

Permalink
Update Logging In section of Testing Your App page (#4885)
Browse files Browse the repository at this point in the history
Co-authored-by: Emily Rohrbough <[email protected]>
Closes #4498
  • Loading branch information
debrisapron committed Dec 6, 2022
1 parent aeaa7c4 commit 4565fcd
Showing 1 changed file with 75 additions and 85 deletions.
160 changes: 75 additions & 85 deletions content/guides/end-to-end-testing/testing-your-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,111 +410,101 @@ You'll likely also want to test your login UI for:

Each of these likely requires a full blown e2e test.

Now, once you have your login completely tested - you may be tempted to think:
#### Reusing the login code

At this point there's nothing stopping you copying and pasting the login code
above into every one of your tests that needs an authenticated user. Or you
could even put all your tests in one big spec file and put the login code in a
`beforeEach` block. But neither of those approaches is particularly
maintainable, and they're certainly not very elegant. A much better solution is
to write a custom `cy.login()` [command](/api/cypress-api/custom-commands).

Custom commands allow you to easily encapsulate and reuse Cypress test logic.
They let you add your own functionality to your test suite and then use it with
the same
[chainable and asynchronous API](guides/core-concepts/introduction-to-cypress#The-Cypress-Command-Queue)
as the built-in Cypress commands. Lets make the above login example a custom
command and add it to `cypress/support/commands.js` so it can be leveraged in
any spec file:

> "...okay great! Let's repeat this login process before every single test!"
<Alert type="danger">

<strong class="alert-header">No! Please don\'t.</strong>

Do not use **your UI** to login before each test.

</Alert>

Let's investigate and tease apart why.

#### Bypassing your UI

When you're writing tests for a very **specific feature**, you _should_ use your
UI to test it.

But when you're testing _another area of the system_ that relies on a state from
a previous feature: **do not use your UI to set up this state**.
```js
// In cypress/support/commands.js

Here's a more robust example:
Cypress.Commands.add('login', (username, password) => {
cy.visit('/login')

Imagine you're testing the functionality of a **Shopping Cart**. To test this,
you need the ability to add products to that cart. Well where do the products
come from? Should you use your UI to login to the admin area, and then create
all of the products including their descriptions, categories, and images? Once
that's done should you then visit each product and add each one to the shopping
cart?
cy.get('input[name=username]').type(username)

No. You shouldn't do that.
// {enter} causes the form to submit
cy.get('input[name=password]').type(`${password}{enter}`, { log: false })

<Alert type="warning">
// we should be redirected to /dashboard
cy.url().should('include', '/dashboard')

<strong class="alert-header">Anti-Pattern</strong>
// our auth cookie should be present
cy.getCookie('your-session-cookie').should('exist')

Don't use your UI to build up state! It's enormously slow, cumbersome, and
unnecessary.
// UI should reflect this user being logged in
cy.get('h1').should('contain', username)
})

Read about [best practices](/guides/references/best-practices) here.
// In your spec file

</Alert>
it('does something on a secured page', function () {
const { username, password } = this.currentUser
cy.login(username, password)

Using your UI to **log in** is the _exact same scenario_ as what we described
previously. Logging in is a prerequisite of state that comes before all of your
other tests.
// ...rest of test
})
```

Because Cypress isn't Selenium, we can actually take a huge shortcut here and
skip needing to use our UI by using [`cy.request()`](/api/commands/request).
#### Improving performance

Because [`cy.request()`](/api/commands/request) automatically gets and sets
cookies under the hood, we can actually use it to build up state without using
your browser's UI, yet still have it perform exactly as if it came from the
browser!
You're probably wondering what happened to our advice about logging in "only
once". The custom command above will work just fine for testing your secured
pages, but if you have more than a handful of tests, logging in before every
test is going to increase the overall run time of your suite.

Let's revisit the example from above but assume we're testing some other part of
the system.
Luckily, Cypress provides the [`cy.session()`](/api/commands/session) command, a
powerful performance tool that lets you cache the browser context associated
with your user and reuse it for multiple tests without going through multiple
login flows! Let's modify the custom `cy.login()` command from our previous
example to use `cy.session()`:

```js
describe('The Dashboard Page', () => {
beforeEach(() => {
// reset and seed the database prior to every test
cy.exec('npm run db:reset && npm run db:seed')

// seed a user in the DB that we can control from our tests
// assuming it generates a random password for us
cy.request('POST', '/test/seed/user', { username: 'jane.lane' })
.its('body')
.as('currentUser')
})

it('logs in programmatically without using the UI', function () {
// destructuring assignment of the this.currentUser object
const { username, password } = this.currentUser

// programmatically log us in without needing the UI
cy.request('POST', '/login', {
username,
password,
})

// now that we're logged in, we can visit
// any kind of restricted route!
cy.visit('/dashboard')

// our auth cookie should be present
cy.getCookie('your-session-cookie').should('exist')

// UI should reflect this user being logged in
cy.get('h1').should('contain', 'jane.lane')
})
Cypress.Commands.add('login', (username, password) => {
cy.session(
username,
() => {
cy.visit('/login')
cy.get('input[name=username]').type(username)
cy.get('input[name=password]').type(`${password}{enter}`, { log: false })
cy.url().should('include', '/dashboard')
cy.get('h1').should('contain', username)
},
{
validate: () => {
cy.getCookie('your-session-cookie').should('exist')
},
}
)
})
```

Do you see the difference? We were able to login without needing to actually use
our UI. This saves an enormous amount of time visiting the login page, filling
out the username, password, and waiting for the server to redirect us _before
every test_.
<Alert type="info">

<strong class="alert-header">Third-Party Login</strong>

Because we previously tested the login system end-to-end without using any
shortcuts, we already have 100% confidence it's working correctly.
If your app implements login via a third-party authentication provider such as
[Auth0](https://auth0.com/) or [Okta](https://www.okta.com/), you can use the
[`cy.origin()`](/api/commands/origin) command to include their login pages as
part of your authentication tests.

</Alert>

Use the methodology above when working with any area of your system that
requires the state to be set up elsewhere. Just remember - don't use your UI!
There's a lot going on here that's out of the scope for this introduction.
Please check out the [`cy.session()`](/api/commands/session) documentation for a
more in-depth explanation.

<Alert type="info">

Expand Down

0 comments on commit 4565fcd

Please sign in to comment.