From 832db607ee0bb38896b141348c1442cde23722cf Mon Sep 17 00:00:00 2001 From: DEBRIS APRON Date: Wed, 30 Nov 2022 15:40:04 -0800 Subject: [PATCH 1/3] Update Logging In section of Testing Your App page --- .../end-to-end-testing/testing-your-app.md | 146 ++++++++---------- 1 file changed, 61 insertions(+), 85 deletions(-) diff --git a/content/guides/end-to-end-testing/testing-your-app.md b/content/guides/end-to-end-testing/testing-your-app.md index 1ec9e65428..62796077cf 100644 --- a/content/guides/end-to-end-testing/testing-your-app.md +++ b/content/guides/end-to-end-testing/testing-your-app.md @@ -410,111 +410,87 @@ 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 -> "...okay great! Let's repeat this login process before every single test!" +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 use a `cy.login()` [custom command](/api/cypress-api/custom-commands). - +Custom commands allow you to easily encapsulate and reuse Cypress test logic. +They're basically just functions, but with some added sugar to allow you to use +them with the same chaining API as the built-in Cypress commands. Here's an +example of the login code from the previous section converted into a custom +command: -No! Please don\'t. - -Do not use **your UI** to login before each test. - - - -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. +```js +// In cypress/support/commands.js -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**. +Cypress.Commands.add('login', (username, password) => { + cy.visit('/login') -Here's a more robust example: + cy.get('input[name=username]').type(username) -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? + // {enter} causes the form to submit + cy.get('input[name=password]').type(`${password}{enter}`) -No. You shouldn't do that. + // we should be redirected to /dashboard + cy.url().should('include', '/dashboard') - - -Anti-Pattern + // 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 - +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 add a lot of running time to 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()` command](/api/commands/session), a +powerful performance tool that lets you cache user session data and reuse it for +multiple tests without going through multiple login flows! Let's modify the +`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}`) + 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_. - -Because we previously tested the login system end-to-end without using any -shortcuts, we already have 100% confidence it's working correctly. - -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 of this introduction, so for +a more in-depth explanation of how `cy.session()` works, +[read the API docs](/api/commands/session). From 5d1d69df233e3da99038a923914d3dc476e612ac Mon Sep 17 00:00:00 2001 From: DEBRIS APRON Date: Mon, 5 Dec 2022 12:27:12 -0800 Subject: [PATCH 2/3] Update content/guides/end-to-end-testing/testing-your-app.md Co-authored-by: Emily Rohrbough --- content/guides/end-to-end-testing/testing-your-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/guides/end-to-end-testing/testing-your-app.md b/content/guides/end-to-end-testing/testing-your-app.md index 62796077cf..4424bd8742 100644 --- a/content/guides/end-to-end-testing/testing-your-app.md +++ b/content/guides/end-to-end-testing/testing-your-app.md @@ -417,7 +417,7 @@ 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 use a `cy.login()` [custom command](/api/cypress-api/custom-commands). +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're basically just functions, but with some added sugar to allow you to use From 8a370a01107bac732a1ef3442c99d290ecf23dca Mon Sep 17 00:00:00 2001 From: DEBRIS APRON Date: Mon, 5 Dec 2022 12:52:39 -0800 Subject: [PATCH 3/3] Address review feedback --- .../end-to-end-testing/testing-your-app.md | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/content/guides/end-to-end-testing/testing-your-app.md b/content/guides/end-to-end-testing/testing-your-app.md index 4424bd8742..a26e3ef039 100644 --- a/content/guides/end-to-end-testing/testing-your-app.md +++ b/content/guides/end-to-end-testing/testing-your-app.md @@ -420,10 +420,12 @@ 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're basically just functions, but with some added sugar to allow you to use -them with the same chaining API as the built-in Cypress commands. Here's an -example of the login code from the previous section converted into a custom -command: +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: ```js // In cypress/support/commands.js @@ -434,7 +436,7 @@ Cypress.Commands.add('login', (username, password) => { cy.get('input[name=username]').type(username) // {enter} causes the form to submit - cy.get('input[name=password]').type(`${password}{enter}`) + cy.get('input[name=password]').type(`${password}{enter}`, { log: false }) // we should be redirected to /dashboard cy.url().should('include', '/dashboard') @@ -461,12 +463,13 @@ it('does something on a secured page', function () { 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 add a lot of running time to your suite. +test is going to increase the overall run time of your suite. -Luckily, Cypress provides the [`cy.session()` command](/api/commands/session), a -powerful performance tool that lets you cache user session data and reuse it for -multiple tests without going through multiple login flows! Let's modify the -`cy.login()` command from our previous example to use `cy.session()`: +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 Cypress.Commands.add('login', (username, password) => { @@ -475,7 +478,7 @@ Cypress.Commands.add('login', (username, password) => { () => { cy.visit('/login') cy.get('input[name=username]').type(username) - cy.get('input[name=password]').type(`${password}{enter}`) + cy.get('input[name=password]').type(`${password}{enter}`, { log: false }) cy.url().should('include', '/dashboard') cy.get('h1').should('contain', username) }, @@ -488,9 +491,20 @@ Cypress.Commands.add('login', (username, password) => { }) ``` -There's a lot going on here that's out of the scope of this introduction, so for -a more in-depth explanation of how `cy.session()` works, -[read the API docs](/api/commands/session). + + +Third-Party Login + +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. + + + +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.