Chapter 7: End-to-End UI Testing with Cypress

In previous chapters, we learned how to test applications at the component level using React Testing Library. In this chapter, we will learn how to test applications at the system level by executing end-to-end testing using Cypress. End-to-end tests play an essential role in helping teams gain the confidence their applications will work as expected for end users in production. By including end-to-end tests in test strategies, teams can gain a lot of knowledge about how applications behave when all dependencies work together. Cypress is a modern, JavaScript end-to-end testing framework that can handle anything that runs in the browser, including applications built with popular frameworks such as React, Angular, and Vue. Cypress features allow teams to install, write, run, and debug tests within minutes.

In addition to system-level testing, it provides the ability to write unit and integration tests, making the framework great for developers and quality engineers. Also, Cypress differs from tools such as Selenium by running tests directly in the browser versus requiring browser drivers, automatically waiting for commands and assertions before proceeding, providing visual feedback for each test command when run, and access to recorded test runs via the Cypress Dashboard.

In this chapter, we're going to cover the following main topics:

  • Installing Cypress in an existing project
  • Enhancing Cypress DOM queries with cypress-testing-library
  • Using Cypress to implement test-driven development
  • Reviewing Cypress design patterns
  • Executing API testing with Cypress
  • Implementing Gherkin-style tests with Cucumber

The knowledge gained in this chapter will add additional test strategies to complement skills learned with React Testing Library.

Technical requirements

For the examples in this chapter, you will need to have Node.js installed on your machine. We will be using the create-react-app CLI tool and the Next.js React framework (https://nextjs.org/) for all code examples. Please familiarize yourself with Next.js before starting the chapter if needed. Code snippets will be provided throughout the chapter to help you understand the code under test, but the objective is understanding how to test the code.

You can find code examples for this chapter here: https://github.com/PacktPublishing/Simplify-Testing-with-React-Testing-Library/tree/master/Chapter07.

Getting started with Cypress

In this section, you will learn how to install and set up Cypress in an existing project. We will also write a test for a user flow. Use the following command at the command line to install Cypress:

npm install cypress --save-dev

The preceding command will install Cypress as a development dependency in your project. Once Cypress is installed, run the following command:

npx cypress open

The preceding command runs the Cypress interactive Test Runner. The Test Runner allows us to manually do things such as select specific tests to run, pick a browser to use for test execution, and see the browser output alongside each associated Cypress command. When we run Cypress in interactive mode for the first time, it creates a suggested folder structure for Cypress projects:

Figure 7.1 – First Cypress open run

Figure 7.1 – First Cypress open run

In the preceding screenshot, Cypress informs us that it automatically created a cypress folder structure on our behalf in the root of the project that includes the following sub-folders – fixtures, integration, plugins, and support. The sub-folders allow us to quickly get up and running without needing to do any manual configuration. The fixtures folder is used to create static data typically used for stubbing network data in tests. The integration folder is used to create test files. Inside the integration folder, Cypress provides an examples folder with numerous examples of using Cypress to test applications.

The plugins folder is used to extend the behavior of Cypress in many ways, such as programmatically changing the config file, generating reports in HTML format after test runs, or adding support for automated visual testing, just to name a few. Cypress provides many out-of-the-box commands such as click, type, and assertions from third-party tools such as Mocha (https://mochajs.org/), Chai (https://www.chaijs.com/), and jQuery (https://jquery.com/).

The support folder is used to create custom commands or add third-party commands with tools such as Cypress Testing Library, which we will learn about in the next section, Enhancing query selectors with Cypress Testing Library. Cypress also creates a cypress.json file at the root of the project folder. The cypress.json file is used to set global settings such as the global base URL Cypress will use in tests, set custom timeouts for elements to appear in the DOM, or even change the folder location of our test files from integration to e2e, for example. There are numerous settings we can configure in the cypress.json file.

In the top-right corner of the Cypress Test Runner is a drop-down list allowing you to select the browser to use for test runs:

Figure 7.2 – Cypress browser dropdown

Figure 7.2 – Cypress browser dropdown

In the preceding screenshot, the Chrome 88, Firefox 80, Edge 88, and Electron 87 version browsers are available to use for test runs. Available browsers are based on Cypress-compatible browsers installed on the user's machine. The Cypress-supported browsers are Firefox and Chrome-family browsers such as Edge and Electron. The Electron browser is available by default in Cypress and is also used for running tests in headless mode, meaning without the browser UI.

To execute a test, simply click the test name from the list of available tests:

Figure 7.3 – Example test run

Figure 7.3 – Example test run

In the preceding screenshot, the actions.spec.js test file located in the examples folder was run. The screen's right side displays the state of the application in the browser throughout each step of the test. The left side of the screen shows the result of each test within the test file. If we wanted to, we could click into each test, hover over each Cypress command, and see the resulting DOM state before and after the command was executed. Being able to hover over each command to view the resulting DOM output is a great feature.

Cypress makes debugging easier as compared to other end-to-end testing frameworks. For example, if Cypress cannot find an element in the browser specified in our test, it provides helpful error messages:

Figure 7.4 – Cypress error output

Figure 7.4 – Cypress error output

In the preceding screenshot, Cypress provides feedback by informing us that an input element named firSDFstName was never found after 4 seconds inside the Test Runner. Cypress also allows us to click a link to open our code editor at the line where the error occurred.

Now that we understand the basics of installing, running, and executing tests with the Cypress Test Runner, we will write a checkout flow test next. When a user checks out, the application progresses through four screens. The first screen is for the shipping address:

Figure 7.5 – Shipping address Checkout screen

Figure 7.5 – Shipping address Checkout screen

In the preceding screenshot, a form is shown where a user can enter their shipping address information. The second screen is for the payment details:

Figure 7.6 – Payment details Checkout screen

Figure 7.6 – Payment details Checkout screen

In the preceding screenshot, a form is shown where a user can enter their payment information. The third screen is for reviewing the order:

Figure 7.7 – Review your order Checkout screen

Figure 7.7 – Review your order Checkout screen

In the preceding screenshot, a summary displaying all form values entered on previous screens is shown. Note, for the purposes of this demonstration, the purchased items T-shirt, Denim Jeans, and Nike Free Runner are hardcoded in the application and will not be a focus in the test we will write. The last screen is the order submitted screen:

Figure 7.8 – Order submitted Checkout screen

Figure 7.8 – Order submitted Checkout screen

In the preceding screenshot, a confirmation is shown, displaying a Thank you message, an order number, and information informing the customer about email communication for order updates.

For the purposes of this demonstration, the order number is hardcoded and will not be a focus of our test. Now that we understand the user flow, we can write the test code:

import user from '../support/user'

describe('Checkout Flow', () => {

  it('allows a user to enter address and payment info and place         an order', () => {

    cy.visit('/')

In the preceding code, we first import a user object to use in the test. The user object simply provides fake values to enter into each form input, so we don't have to hardcode each value. Next, we use the visit command via the global cy variable to visit the application.

All available Cypress methods are chained off the cy variable. Note that the '/' used in the visit method represents the URL relative to our tests' base URL. By using a relative URL, we don't have to enter the full URL in our tests. We can set the baseURL property via the cypress.json file:

{

  "baseUrl": "http://localhost:3000"

}

In the preceding code, we set baseUrl to http://localhost:3000, allowing us to use '/' when we want to visit the index page or other pages relative to the index page.

Next, we will write the code to complete the Shipping address screen:

    cy.get('input[name="firstName"]').type(user.firstName)

    cy.get('input[name="lastName"]').type(user.lastName)

    cy.get('input[name="address1"]').type(user.address1)

    cy.get('input[name="city"]').type(user.city)

    cy.get('input[name="state"]').type(user.state)

    cy.get('input[name="zipCode"]').type(user.zipCode)

    cy.get('input[name="country"]').type(user.country)

    cy.contains(/next/i).click()

In the preceding code, we use the get command to select each input element via its name attribute. We also use the type command to enter a value for each input. Next. We use the contains command to select the button element with the text next and click it using the click command.

Next, we will enter values for the Payment details screen:

    cy.get('input[name="cardType"]').type(user.cardType)

    cy.get('input[name="cardHolder"]').type(user.cardHolder)

    cy.get('input[name="cardNumber"]').type(user.cardNumber)

    cy.get('input[name="expiryDate"]').type(user.expiryDate)

    cy.get('input[name="cardCvv"]').type(user.cardCvv)

    cy.contains(/next/i).click()

In the preceding code, we use the get and type commands to select and enter values in each input. Then, we use the contains command to click the next button.

Next, we will verify entered values for shipping and payment details on the Review your order screen:

    cy.contains(`${user.firstName}

      ${user.lastName}`).should('be.visible')

    cy.contains(user.address1).should('be.visible')

    cy.contains(`${user.city}, ${user.state}

     ${user.zipCode}`).should(

      'be.visible'

    )

    cy.contains(user.country).should('be.visible')

    cy.contains(user.cardType).should('be.visible')

    cy.contains(user.cardHolder).should('be.visible')

    cy.contains(user.cardNumber).should('be.visible')

    cy.contains(user.expiryDate).should('be.visible')

    cy.contains(/place order/i).click()

We use the contains command to select each element via form values entered on previous screens in the preceding code. We also use the should command to assert that each element is visible on the screen. Then, we use the contains command to select the button with the text place order and click it using the click command.

Finally, we verify the application lands on the order submitted screen:

     cy.contains(/thank you for your

       order/i).should('be.visible')

In the preceding code, we use the contains and should commands to verify an element with the text Thank you for your order. visible on the screen. To run the test, we can use the npx cypress open command directly at the command line as previously learned at the beginning of this section, but we can also create an npm script:

    "cy:open": "cypress open",

In the preceding code, we create a cy:open script to run the Cypress Test Runner. We can also create another script to run tests in headless mode:

    "cy:run": "cypress run",

We create a cy:run script to run Cypress in headless mode via the cypress run command in the preceding code. We can use the cy:run script in situations where we don't want to use the interactive mode, such as running via a Continuous Integration and Continuous Delivery (CI/CD) server. Before running Cypress, be sure that your development server is already up and running because Cypress does not start the development server for you. When we execute the test via the cy:open interactive mode, we get the following output:

Figure 7.9 – Checkout flow test results

Figure 7.9 – Checkout flow test results

In the previous screenshot, the test run indicates the checkOutFlow test passed as expected. Now you know how to install and use Cypress to test a user flow. In the next section, we will install a plugin to enhance our element selector commands.

Enhancing Cypress commands with the Cypress Testing Library

In the previous section, we learned how to install and write a user flow test using Cypress. In this section, we will learn how to install and configure the Cypress Testing Library to add enhanced query selectors. The Cypress Testing Library will allow us to use DOM Testing Library query methods in Cypress. Install the library using the following command:

npm install --save-dev @testing-library/cypress

The preceding code installs @testing-library/cypress as a development dependency in your project. After the library is installed, we can add it to the Cypress commands file:

import '@testing-library/cypress/add-commands'

In the preceding code, we extended the Cypress commands with those from the Cypress Testing Library. Now that we have the Cypress Testing Library installed, we can use it in our tests. It should be noted that only findBy* methods from the DOM Testing Library are included to support the Cypress retry-ability feature that retries commands a number of times before timing out.

In the Getting started with Cypress section of this chapter, we wrote a test for a checkout flow. We can refactor element queries in that test with those from the Cypress Testing Library. For example, we can refactor the code for the Shipping address screen like so:

    cy.findByRole('textbox', { name: /first name/i

      }).type(user.firstName)

    cy.findByRole('textbox', { name: /last name/i

      }).type(user.lastName)

    cy.findByRole('textbox', { name: /address line 1/i

      }).type(user.address1)

    cy.findByRole('textbox', { name: /city/i

      }).type(user.city)

    cy.findByRole('textbox', { name: /state/i

      }).type(user.state)

    cy.findByRole('textbox', { name: /postal code/i

      }).type(user.zipCode)

    cy.findByRole('textbox', { name: /country/i

      }).type(user.country)

    cy.findByText(/next/i).click()

In the previous code, we updated all selectors to find input elements by their Accessible Rich Internet Application (ARIA) textbox role using findByRole queries. ARIA attributes are used by individuals using assistive technology to locate elements. We also updated the selector for the next button by using the findByText query. The same refactoring pattern is used for the Payment details and Review your order screens. Finally, we can refactor the code for the order submitted screen like so:

        cy.findByRole('heading', { name: /thank you for

          your order/i }).should(

      'be.visible'

    )

    cy.findByRole('heading', { name: /your order number is

      #2001539/i }).should(

      'be.visible'

    )

In the previous code, we updated the two selectors to find elements by their heading role using the findByRole query. Our test code now queries elements in ways that are more accessible, providing more confidence that the application will work for all users, including those using assistive technology such as screen readers. Also, the test code reads better when viewing each line in the Test Runner screen.

Now you know how to install the Cypress Testing Library and refactor existing tests using queries that avoid using implementation details. In the next section, we will learn how to use test-driven development with Cypress to add features to a blog application.

Cypress-driven development

In the previous section, we installed the Cypress Testing Library and refactored an existing test for a checkout flow. In this section, we will use Cypress to drive the development of new features for an existing blog application created with Next.js. Next.js is a popular framework that provides a pleasant experience for teams to build static or server-rendered React applications.

Example features that Next.js provides are out-of-the-box routing, built-in CSS support, and API routes. Please see the Next.js documentation (https://nextjs.org/) for more details. The MY BLOG application currently has two pages, a Home page displaying all blog posts and a page to display blog details. The page that displays a list of posts looks as follows:

Figure 7.10 – Blog home page

Figure 7.10 – Blog home page

In the previous screenshot, the Home page displays two blog posts, I love React and I love Angular. Blog data is stored in a MongoDB database and sent to the frontend via the API once the application loads. Each blog post displays a category, title, published date, an excerpt, and a Continue Reading link from top to bottom.

To view a blog's details, a user can click either the blog title or the Continue Reading link. For example, we see the following after clicking the I love React title:

Figure 7.11 – Blog detail page

Figure 7.11 – Blog detail page

We see the full content of the I love React blog post in the preceding screenshot. We also see the published date and a BACK TO BLOG link to navigate back to the Home page. The application's current state only allows new blog posts to be created via a POST request to the API or directly adding new posts to the database.

We need to add a feature that allows users to add new posts via the UI. We can use Cypress to write a test for the expected behavior and build out the UI little by little until the feature is complete and the test passes. The following test shows the final expected behavior:

import fakePost from '../support/generateBlogPost'

describe('Blog Flow', () => {

    let post = {}

  beforeEach(()=> (post = fakePost()))

  it('allows a user to create a new blog post', () => {

    cy.visit('/')

    cy.findByRole('link', { name: /new post/i }).click()

In the previous code, first, we import fakePost, a custom method that will generate unique test data for each test run and set it as the value for the variable post. We don't want to create identical blog posts, so the custom method helps by always creating unique data. Next, we visit the Home page and click a link with the name New Post. The New Post link should navigate us to a page where we can enter values for a new post.

Next, we test the code for entering values for the new post:

    cy.findByRole('textbox', { name: /title/i

      }).type(post.title)

    cy.findByRole('textbox', { name: /category/i

      }).type(post.category)

    cy.findByRole('textbox', { name: /image link/i

      }).type(post.image_url)

    cy.findByRole('textbox', { name: /content/i

      }).type(post.content)

In the preceding code, we find each textbox element by its unique name and enter associated values via the custom post method. Finally, we create the last pieces of the test:

    cy.findByRole('button', { name: /submit/i }).click()

    cy.findByRole('link', { name: post.title

     }).should('be.visible')

  })

})

In the preceding code, we click the submit button. Once we click the submit button, the data should be sent to the API, saved to the database, and then the application should navigate us back to the Home page. Finally, once on the Home page, we verify the title for the post we created is visible on the screen.

We will run the test using the Cypress Test Runner to utilize its interactive features and keep it open throughout building the feature. Our test will fail as expected when run:

Figure 7.12 – Blog Flow test failure

Figure 7.12 – Blog Flow test failure

In the previous screenshot, the first step succeeded in navigating to the Home page, but the output informs us that the test failed because a link element with the name New Post was not found after 4 seconds in the second test step. Four seconds is the default time that Cypress will continue to query for the element before timing out.

We also see a helpful message from the DOM Testing Library informing us which accessible elements are visible in the DOM. Further, we can look at the browser at the point of test failure and see that the New Post link is not visible. Now we can update the UI to make the second test step pass:

<Link href="/add">

<a className="font-bold inline-block px-4 py-2 text-3xl">

  New Post

</a>

</Link>

In the previous code, we added a link that will navigate the user to an Add a new blog page when clicked. Notice the hyperlink element is wrapped in a Link component. The Link component allows for client-side route navigation. The Test Runner automatically reruns when we save the test file. Since we already wrote all the necessary test code, we can trigger a test run by saving the file.

We will need to perform this action after each UI change. Now we get the following output when the test runs:

Figure 7.13 – Blog Flow add page failure

Figure 7.13 – Blog Flow add page failure

In the previous screenshot, our test code can now successfully open the Home page, click the New Post link and land on the Add page. However, now the output indicates our test failed because an element with the name title and role textbox was not found at step 4. We can update the UI by creating an input element with the name title and the textbox role:

<label htmlFor="title">Title</label>

<input

   type="text"

   autoFocus

   id="title"

   name="title"

   placeholder="Blog Title"

   value={newBlog.title}

   onChange={handleChange}

/>

In the previous code, we add a Title label element and an associated input element of type text. Although not demonstrated in the last code, we also went ahead and added the Category, Image link, and Content input elements similar in structure to the Title input element. Now we get the following output when we trigger a test run:

Figure 7.14 – Blog Flow add page input element refactor

Figure 7.14 – Blog Flow add page input element refactor

In the previous screenshot, our test code can now successfully open the Home page. Click the New Post link, and add values in the Title, Category, Image link, and Content input elements on the Add page. However, now the output indicates our test failed because a Submit button was not found at step 12. We can update the UI by creating the Submit button:

<button>Submit</button>

We created and added a Submit button to the Add page in the previous code. The Submit button is part of the form element and, when clicked, calls a method that sends the form data to the API and, ultimately, the database. Although not a focus for our test, we also added a cancel button element in the UI. Now we get the following output when we trigger a test run:

Figure 7.15 – Blog Flow add page completed refactor

Figure 7.15 – Blog Flow add page completed refactor

In the previous screenshot, the output indicates the test finally passes. We can see the new blog post in the browser created by our test on the screen's right side. With the last refactor, we have completed all the feature steps that allow users to add new posts via the UI.

For our next feature, we want users to have the ability to delete blog posts via the UI. We will add a delete link to the blog detail page that makes a DELETE request to the API when clicked. The application's current state only allows blog posts to be deleted via a DELETE request to the API or directly in the database. Our previous test can be updated to perform actions to delete the new blog post after creation like so:

    cy.findByRole('link', { name: post.title }).click()

    cy.findByText(/delete post>/i).click()

    cy.findByRole('link', { name: post.title

     }).should('not.exist')

In the preceding code, first, we click the title of the blog post to delete to navigate to its detail page. Next, we find and click the link with the text delete post. Finally, we verify the post is no longer in the list of blog posts on the Home page. We get the following output when we trigger a test run by saving the file:

Figure 7.16 – Blog Flow delete post test failure

Figure 7.16 – Blog Flow delete post test failure

In the previous screenshot, the output indicates the test failed at step 18 when an element with the text delete post could not be found. We can update the UI by creating the missing element:

<a onClick={handleDelete}>Delete post&#62;</a>;

In the preceding code, we add a hyperlink element with the text Delete post. When the hyperlink is clicked, it calls a handleDelete method to send a DELETE request to the API and ultimately remove the blog post from the database. We get the following output when we save the test file to trigger a test run:

Figure 7.17 – Blog Flow delete post completed refactor

Figure 7.17 – Blog Flow delete post completed refactor

In the previous screenshot, the output indicates the test finally passes with the blog post deleted. With the addition of the delete link, we have completed all the feature steps that allow users to delete blog posts via the UI. Now you know how to develop features using Cypress-driven development.

The approach can be beneficial when you want to see the application in a specific state as you build out a feature. In the next section, we will cover Cypress design patterns.

Writing Tests using Cypress design patterns

In the previous section, we learned how to use Cypress to drive the development of new features to a blog application. In this section, we will look at two design patterns to structure our Cypress code. Design patterns help teams by providing solutions to problems such as writing maintainable code or designing responsive websites. First, we will look at the Page Object Model, followed by custom commands.

Creating page objects in Cypress

The Page Object Model (POM) is a popular design pattern commonly used in Selenium test frameworks to increase readability and maintainability for end-to-end tests. The POM model consists of creating an object-oriented class representation for each page in an application, including custom methods to select and interact with various page elements. An advantage of using the POM model is abstracting away multiple lines of test code inside a single method.

Also, page objects serve as a single source of truth for actions performed on specific pages. In the Cypress-driven development section, we added a feature to allow users to create a new blog post through the UI. We can refactor the test code using the POM pattern. First, we will create a page object for the Home page:

class HomePage {

  navigateToHomePage() {

    cy.visit('/')

  }

  navigateToAddPage() {

    cy.findByRole('link', { name: /new post/i }).click()

  }

  getBlogPost(post) {

    return cy.findByRole('link', { name: post.title })

  }

}

export const homePage = new HomePage()

In the preceding code, first, we create a page object for the Home page with navigateToHomePage, navigateToAddPage, and getBlogPost methods. Then, we export a new instance of the object to use in test files. Next, we will create a page object for the Add page:

class AddPage {

  createNewPost(newPost) {

    cy.findByRole('textbox', { name: /title/i

     }).type(newPost.title)

    cy.findByRole('textbox', { name: /category/i

     }).type(newPost.category)

    cy.findByRole('textbox', { name: /image link/i

     }).type(newPost.image_url)

    cy.findByRole('textbox', { name: /content/i

     }).type(newPost.content)

    cy.findByRole('button', { name: /submit/i }).click()

  }

}

export const addPage = new AddPage()

In the preceding code, we create a page object for the Add page with a createNewPost method that accepts a newPost object with data to enter for the new post. The page object is exported for use in test files. Now that we have page objects representing the Home and Add pages, we can use them in a test:

import post from '../support/generateBlogPost'

import { addPage } from './pages/AddPage'

import { homePage } from './pages/HomePage'

In the preceding code, first, we import the fake post method to generate unique post data in the test. Next, we import the addPage and homePage page objects. Next, we will write the main test code:

      it('POM: allows a user to create a new blog post', ()

       => {

    homePage.navigateToHomePage()

    homePage.navigateToAddPage()

    addPage.createNewPost(post)

    homePage.getBlogPost(post).should('be.visible')

  })

In the preceding code, first, we navigate to the Home page. Next, we navigate to the Add page. Then, we create a new post with values from the fake post method. Finally, we get the new post on the Home page and verify that it is visible on the screen.

In the Cypress-driven development section, we added another feature to delete blog posts through the UI. We can add a method for this feature in our page objects and verify our test's behavior. First, we will add a new method to the homePage page object:

navigateToPostDetail(post) {

cy.findByRole('link', { name: post.title }).click()

}

In the previous code, we added a navigateToPostDetail method that accepts a post argument when called. Next, we will create a page object for the Post Detail page:

class PostDetailPage {

  deletePost() {

    cy.findByText(/delete post>/i).click()

  }

}

export const postDetailPage = new PostDetailPage()

In the preceding code, we created a page object for the Post Detail page and added a deletePost method. We also exported an instance of the page object to use in tests. Now we can use the new page object methods in our existing test:

import { postDetailPage } from './pages/PostDetailPage'

In the previous code, first, we import the postDetailPage page object similar to how we did with other page objects. Next, we will add the associated methods to delete the post:

    homePage.navigateToPostDetail(post)

    postDetailPage.deletePost()

    homePage.getBlogPost(post).should('not.exist')

In the preceding code, we invoked the navigateToPostDetail and deletePost methods and verified the post no longer exists on the Home page. Now our task of refactoring the test code to page objects is completed. Our test code is shorter and abstracts away many test step details.

However, our page object design does present an issue if we split the add blog post and delete blog post features into two different tests. The first test will create a blog post:

  it('POM: allows a user to create a new blog post', () => {

    homePage.navigateToHomePage()

    homePage.navigateToAddPage()

    addPage.createNewPost(post)

    homePage.getBlogPost(post).should('be.visible')

  })

In the preceding code, the test 'POM: allows a user to create a new blog post' creates a blog post. Next, we will create the test to delete the blog post:

  it('POM: allows a user to delete a new blog post', () => {

    homePage.navigateToHomePage()

    homePage.navigateToAddPage()

    addPage.createNewPost(post)

    homePage.navigateToPostDetail(post)

    postDetailPage.deletePost()

    homePage.getBlogPost(post).should('not.exist')

  })

In the preceding code, the test 'POM: allows a user to delete a new blog post' deletes a blog post. The Delete test's problem is that we have to write many of the same test steps from the previous test and the actions most important to the test to delete the post. As a testing best practice, we want to avoid writing the same test steps in multiple tests.

In the next section, we will learn how to resolve this problem with custom Cypress commands.

Creating custom Commands in Cypress

In the previous section, we learned how to write tests using the POM pattern. However, we came across an issue where we had to write the same test steps in a different test. Cypress provides a custom command feature to resolve the issue. Custom commands allow us to add additional commands to Cypress. In the Enhancing Cypress commands with the Cypress Testing Library section, we added third-party custom commands. Now we will learn how to write our own custom commands. First, we will create a custom method to create a new blog post:

Cypress.Commands.add('createBlogPost', post => {

  cy.visit('/')

  cy.findByRole('link', { name: /new post/i }).click()

  cy.findByRole('textbox', { name: /title/i

   }).type(post.title)

  cy.findByRole('textbox', { name: /category/i

   }).type(post.category)

  cy.findByRole('textbox', { name: /image link/i

   }).type(post.image_url)

  cy.findByRole('textbox', { name: /content/i

   }).type(post.content)

  cy.findByRole('button', { name: /submit/i }).click()

   })

In the preceding code, we add a custom createBlogPost command to Cypress via the Commands.add method inside the commands.js file. Next, we will use the custom method in our test:

  it('Custom Command: allows a user to delete a new blog

    post', () => {

    cy.createBlogPost(post)

    homePage.navigateToPostDetail(post)

    postDetailPage.deletePost()

    homePage.getBlogPost(post).should('not.exist')

  })

In the preceding code, we replace the previous code that creates a new blog post with the custom createBlogPost method we created. The custom method eliminates the need to explicitly write the same code lines to create a blog post. We can use the custom method in any future test when needed. However, for our specific test to delete a blog post, we can go a step further.

Although our custom createBlogPost method eliminates the need to write duplicate lines of code, we are still performing the same steps to create a new blog post via the UI. Executing the same steps in multiple tests is a bad testing practice as we are repeating steps we've already tested. If we have controllable access to our application's API, we can reduce repeated steps through the UI.

Cypress provides an HTTP request client that we can use to communicate with the API directly. Using the request client, we can bypass the UI to avoid repeating steps already tested and speed up our test. We can refactor our custom createBlogPost method like so:

  cy.request('POST', '/api/add', post).then(response => {

    expect(response.body.message).to.equal(

      `The blog "${post.title}" was successfully added`

    )

  })

In the previous code, we use the request method to make a POST request to the API at /api/add and send a post object containing values for the new post. Then we assert the server sends back the message The blog "blog title here" was successfully added, indicating the new post was added to the database. Note that "blog title here" in the message would be replaced with the blog post's real title when the request is made. Now we can update our test code:

    cy.createBlogPost(post)

    homePage.navigateToHomePage()

    homePage.navigateToPostDetail(post)

    postDetailPage.deletePost()

    homePage.getBlogPost(post).should('not.exist')

In the previous code, our test looks almost identical to the previous version. The only change is the implementation of the createBlogPost method and adding the navigateToHomePage method. However, now the test will run faster because we skip creating a new blog post through the UI. Although we used the POM pattern along with custom commands in this section, it should be noted that we could have solely used custom commands.

We only need to test the add blog post and delete blog post features in one unique test to add the confidence they will work as expected for users. If tagged as critical user flows, the tests could run again in regression test suites to ensure the features continue to work as new features are added. We could write the Cypress commands to interact with the application directly without using the POM pattern and use custom commands in situations where we have to rerun the same steps.

Now you know how to structure maintainable test code and reduce duplicate steps by implementing the POM pattern and custom Cypress commands.

In the next section, we will build our knowledge of the Cypress request client by testing our application's API routes.

Testing APIs with Cypress

In the previous section, we learned how to structure test code using the POM and custom commands design patterns. We also learned that we could use Cypress to interact with our application's API directly. In this section, we will build on the previous section's learnings by testing the API of the blog application previously introduced in the Cypress-driven development section.

The blog application accepts four API requests: a GET request to get all posts, a POST request to add a post, a POST request to get a single post, and a DELETE request to delete a post. First, we will test the GET request for all posts:

import fakePost from '../support/generateBlogPost';

  const post = fakePost()

const getAllPosts = () => cy.request('/api/posts').its('body.

posts');

const deletePost = (post) =>

  cy.request('DELETE', `/api/delete/${post.id}`, {

    id: post.id,

    name: post.title,

  });

const deleteAllPosts = () => getAllPosts().each(deletePost);

beforeEach(deleteAllPosts);

In the preceding code, first, we import the fakePost method used to generate dynamic post data for each test run and assign it to the variable post. Next, we create three test setup methods: getAllPosts, deletePost, and deleteAllPosts. Before each test run, we want to start with an empty database.

The deleteAllPosts method will get all current posts from the database via getAllPosts, which calls deletePost to delete each post. Finally, we pass deleteAllPosts to beforeEach, which will call deleteAllPosts before each test run. Next, we will write the main code for the get all posts request:

    cy.request('POST', '/api/add', {

      title: post.title,

      category: post.category,

      image_url: post.image_url,

      content: post.content

    })

    cy.request('/api/posts').as('posts')

    cy.get('@posts').its('status').should('equal', 200)

    cy.get('@posts').its('body.posts.length').should('equal',

  1)

In the preceding code, we first use the request method to add a new blog post to the API to save in the database. Next, we use request to get all posts from the database. Since we wiped the database before the test, we should receive the one blog post we just created from the database.

We use the as method, a Cypress feature that allows us to save a code line as an alias. Then, we use the get method to access the alias using the required @ symbol before the alias name to verify the API server's response status code is 200. Finally, we assert that the length of the posts body is 1. Next, we will test the create new blog post request:

    cy.request('POST', '/api/add', post).as('newPost')

    cy.get('@newPost').its('status').should('equal', 200)

    cy.get('@newPost')

      .its('body.message')

      .should('be.equal', `The blog "${post.title}" was

        successfully added`)

In the preceding code, first, we created a new blog post and saved the result as an alias labeled newPost. Then, we verify the API response status is 200 and that the response message is The blog "title here" was successfully added, where "title here" would be equal to the actual title in the test. Next, we will test the delete a post request:

    cy.request('POST', '/api/add', post)

getAllPosts().each(post =>

  cy

    .request('DELETE', `/api/delete/${post.id}`, {

      id: post.id,

      title: post.title

    })

    .then(response => {

      expect(response.status).equal(200)

      expect(response.body.message).equal(

        `post "${post.title}" successfully deleted`

      )

    })

)

In the preceding code, we add a new post similar to what we did in previous tests. Then, we use getAllPosts to requests all current posts, which is only one, and make a DELETE request to remove each one from the application. Then, we verify the API sends a status of 200 indicating successful deletion.

Finally, we verify the API sends a response message that provides textual confirmation that the post has been deleted. For the final test, we will verify the get a single post request:

    cy.request('POST', '/api/add', post)

    getAllPosts().each(post =>

      cy

        .request(`/api/post/${post.id}`)

        .its('body.post.title')

        .should('equal', post.title)

    )

  })

In the preceding code, first, we create a new post similar to previous tests. Then, we get all posts and verify the post title sent back from the API matches the title of the created post. Now you know how to test APIs using Cypress. It is great knowing that Cypress provides features to perform end-to-end testing for the API and UI, all in the same framework.

In the next section, we will learn how to create Gherkin-style test scenarios using Cucumber.

Writing Gherkin-style tests with Cucumber

In the previous section, we learned how to use Cypress to test API responses. In this section, we will learn how to create Gherkin-style tests with Cucumber. Gherkin is a behavior-driven development language used by Cucumber to describe test scenarios' behavior in a plain-English format. Tests written in Gherkin also make it easier for software teams to communicate and provide context for test cases with business leaders.

Gherkin uses the following keywords: Feature, Scenario, Given, When, and Then. Feature is used to describe the thing to build, such as a login page, for example. Scenario describes the user flow for the feature. For example, a user can enter a username, password, and click Login to navigate to their profile page.

The Given, When, and Then keywords describe the scenario at different stages. We could write a complete Gherkin test for a login feature like so:

Feature: Login

  Scenario: A user can enter a username, password, and

    click login to navigate to their profile page.

    Given I am on the login page

    When I enter a username

    When I enter a password

    When I click "login"

    Then I am navigated to my profile page

In the previous code, we created a Gherkin test for a login feature. We can use the cypress-cucumber-preprocessor plugin to write Gherkin-style tests using Cypress. Install the plugin using the following command:

npm install --save-dev cypress-cucumber-preprocessor

The previous command installs the cucumber plugin as a development dependency in your project. Once the plugin is installed, we can configure it for use in our Cypress project:

const cucumber = require('cypress-cucumber-

  preprocessor').default

module.exports = (on, config) => {

  on('file:preprocessor', cucumber())

}

In the preceding code, we add the cucumber plugin to the Cypress plugins file. Now the cucumber plugin features can be used in our tests. Next, we will add the plugin's feature file type to our global configuration file:

{

  "testFiles": "**/*.feature"

}

In the preceding code, we configure Cypress to use files with the feature extension as the test files. Next, we will add a section to our package.json file specifically to load the configuration for the cucumber plugin in our project and tell the plugin where to find our feature files:

  "cypress-cucumber-preprocessor": {

    "nonGlobalStepDefinitions": true,

    "stepDefinitions": "./cypress/e2e"

  }

In the preceding code, we added the necessary configuration code to our package.json file. Now that Cucumber is configured in our project, we will use it to write a test for the user flow of creating and deleting a blog post for the blog application previously introduced in the Cypress-driven development section. First, we will create a feature file:

Feature: Blog Application

  Scenario: A user can create a blog post.

    Given I am on the home page

    When I click the "New Post" link

    When I fill out the new blog form

    When I click "Submit"

    Then I see the new post on the home page

In the preceding code, we create a feature file for the scenario where a user creates a blog post. Next, we will write the associated code for the Gherkin steps:

import { Given, Then, When } from 'cypress-cucumber-

  preprocessor/steps'

import post from '../../support/generateBlogPost'

const currentPost = post

Given('I am on the home page', () => {

  cy.visit('/')

})

When('I click the "New Post" link', () => {

  cy.findByRole('link', { name: /new post/i }).click()

})

In the preceding code, first, we import the Given, Then, and When methods from the Cypress Cucumber library. Next, we import the fake post method to generate test data. Since each test step will live in its own method, we store the fake post data to maintain the same post throughout the test. Then, we use the Given method to create the first test step. The step name: I am on the home page must match the feature file's same words. Inside the Given method, we write the Cypress code associated with the step. Next, use the When method to create the next step. Next, we will add the following step definitions:

When('I fill out the new blog form', () => {

  cy.findByRole('textbox', { name: /title/i

   }).type(currentPost.title)

  cy.findByRole('textbox', { name: /category/i

   }).type(currentPost.category)

  cy.findByRole('textbox', { name: /image link/i

   }).type(currentPost.image_url)

  cy.findByRole('textbox', { name: /content/i

   }).type(currentPost.content)

})

When('I click "Submit"', () => {

  cy.findByRole('button', { name: /submit/i }).click()

})

In the preceding code, we used the When method to write the associated code for the I fill out the new blog form and I click "Submit" steps. Finally, we use the Then method to create the final step definition:

Then('I see the new post on the home page', () => {

  cy.findByRole('link', { name: currentPost.title

   }).should('be.visible')

})

In the preceding code, we use the Then method to create the associated code for the I see the new post on the home page step. We will create a Cucumber test for the delete a blog post user flow for the next test.

First, we make the Gherkin feature scenario:

  Scenario: A user can delete a blog post.

    Given I am on the home page

    When I click the blog post name link

    When I click the delete link

    Then the post is removed from the home page

In the preceding code, we create a scenario for deleting a blog post. Next, we will write the associated step definitions:

When('I click the blog post name link', () => {

  cy.findByRole('link', { name: currentPost.title

   }).click()

})

When('I click the delete link', () => {

  cy.findByText(/delete post>/i).click()

})

In the preceding code, we use the When method to add the associated test code for the I click the blog post name link and I click the delete link steps. Finally, we use the Then method to create the the post is removed from the home page step:

Then('the post is removed from the home page', () => {

  cy.findByRole('link', { name: currentPost.title

   }).should('not.exist')

})

In the preceding code, we add the test code associated with the last step to verify the deleted post is removed from the Home page. Notice that we didn't need to create another method for the I am on the home page step. Cucumber is smart enough to use any step definition that matches the string of text in the feature file.

Now you know how to write Gherkin-style tests in Cypress using Cucumber. You can do other things with Cucumber, such as adding tags to run specific tests and creating data tables that allow you to test multiple arguments for similar Gherkin steps.

Using React Developer Tools with Cypress

In the previous section, we learned how to write tests using Cucumber. In this section, we will learn how to install React Developer Tools for development. React Developer Tools is a great tool to have while developing React applications. It enables you to inspect the hierarchy of components rendered in the DOM and do things such as viewing and editing component props and state. There are Chrome and Firefox extensions available to install React Developer Tools. There is also a standalone Electron app version, which is useful, such as when you want to debug React applications in Safari or mobile browsers. We will also learn how to use the standalone version with Cypress.

Use the following command to install via the command line:

npm install --save-dev react-devtools

The preceding command will install react-devtools as a development dependency in your project. Next, we need to add a script that will connect react-devtools to your application. If you are building a Next.js application, install the special <script src="http://localhost:8097"> script in the Head component in the _document.js file:

<Head>

  <script src="http://localhost:8097"></script>

</Head>

In the preceding code, we added the script inside the Head component. The script ensures React Developer Tools connects to your Next.js application. If you are building an application using create-react-app, install the special script in the head element of the index.html file located in the public folder:

<!DOCTYPE html>

<html lang="en">

  <head>

    <script src="http://localhost:8097"></script>

In the preceding code, we add the script as the first thing inside the head element. We need to remember to remove the special react-devtools script before deploying the application to production because it is a development tool that would add unnecessary code to our production-versioned application.

After the script has been added, next we will create an npm script in the package.json file to start the tool:

"scripts": {

  "devtools": "react-devtools"

In the preceding code, we added a devtools script to run react-devtools. Now that we have a script to run the tool, the last thing we need to do is start our application, the Cypress interactive tool, and react-devtools: each in a separate tab at the command line.

For Next.js applications, use the following command:

npm run dev

We ran the preceding command to start the Next.js application in development. For create-react-app applications, use the following command:

npm start

We ran the preceding command to start the create-react-app application in development. In the Getting started with Cypress section, we created a "cy:open" script to start Cypress in interactive mode. We can run the script like so:

npm run cy:open

In the preceding command, we ran the script to start Cypress. The next thing we need to do is run the react-devtools script:

npm run devtools

In the preceding command, we ran the script to start react-devtools. When run, react-devtools opens its application on our computer:

Figure 7.18 – React Developer Tools application

Figure 7.18 – React Developer Tools application

In the preceding screenshot, react-devtools opens and listens for our application to run to connect to it. Once we run any of our Cypress tests via the interactive mode, the applications component tree will populate inside of the react-devtools application:

Figure 7.19 – React Developer Tools component tree view

Figure 7.19 – React Developer Tools component tree view

In the preceding screenshot, the react-devtools application displays the resulting component tree of the running test. With the application running, we have many tools available, such as clicking on component names to view related information:

Figure 7.20 – React Developer Tools component details

Figure 7.20 – React Developer Tools component details

In the preceding screenshot, we select one of the Link components on the left side of the react-devtools screen. When we click the component, it displays associated information on the react-devtools screen's right side, such as props and hooks. We also see the Cypress interactive mode screen on the right side of the screenshot.

Now you know how to use React Developer Tools with Cypress. In addition to the debugging tools provided by Cypress, you now have an extra tool to debug React applications while running Cypress.

Summary

In this chapter, you have learned new strategies to test applications using Cypress. You can write end-to-end tests to verify critical user flows for applications. You learned how to implement API testing. You now know the benefits of using Cypress-driven development to create new features. You understand the POM and custom command design patterns to structure and organize test code.

Finally, you learned how to use Cucumber to write Gherkin-style tests that enhance communication with non-technical team members.

Congratulations, you have reached the end of our journey and now know about numerous strategies and tools to simplify testing React applications! The concepts and skills gained in this book will help you write quality code no matter what JavaScript project you tackle in the future.

Good luck, and always remember, no great software is built without a foundation of great tests.

Questions

  1. Find a previous project and install and write a suite of end-to-end tests with Cypress.
  2. Create a CRUD API and test it using Cypress.
  3. Build a full-stack React application and write as many tests as you can think of using as many different strategies gained from this book as possible.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.230.147.225