Intro to Cypress Testing

Prework:

Before this lesson, be sure you have worked through this prework to complete the necessary set-up steps.

When we start the lesson you should have the Feedback Loop api ready and running and the Feedback Loop UI ready and running - with Cypress installed and the 3 specific spec files set up in your e2e directory.

Learning Goals:

  • Become familiar with what Cypress is
  • Practice testing a React application with Cypress including:
    • Filling out forms and switching routes
    • Intercepting and stubbing network requests
    • Happy and sad path user flows
  • Understand how acceptance testing & end-to-end testing differ from unit & integration testing

What is Cypress?

Cypress is an automated testing tool used for the functional aspects of web applications.

The Big Picture

We’ll be using Cypress in two main ways:

  1. Simulating the user’s interactions with the various features of our application, making assertions about when and where the user will encounter the various elements on the DOM
  2. Intercepting the real network requests our application makes, and simulating a mock response from our API when a real network request is made.

Consider the following code and write down your responses to the questions:

  • Which parts of this code align with the 1st way we’ll be using Cypress? How do you know?
  • Which parts of this code align with the 2nd way we’ll be using Cypress? How do you know?
  • What are some reasons we might want to avoid hitting our API with our test suite?
  • How do these tests give us confidence that our app is working as intended?
  beforeEach(() => {
    cy.intercept("GET", "https://api.openbrewerydb.org/breweries?by_city=savannah", {
      statusCode: 200,
      fixture: "savannah_breweries"
    })
    .visit("http://localhost:3000/")
  });

  it("should have a form to enter a city and display that city's breweries", () => {
    cy.get("input[name='city']").type("savannah")
    .get(".search-button").click()
    .get(".breweries-container").find(".brewery-card-wrapper").should("have.length", 2)
    .get(".brewery-name").first().contains("h2", "Moon River Brewing Co")
    .get(".brewery-name").last().contains("h2", "Two Tides Brewing Company")
    .get(".brewery-location").first().contains("p", "Savannah, Georgia")
  });

With the two main ways we’ll be using Cypress in mind, take some time to play around with the Feedback Loop UI application as a user. Try to identify and list some user flows within this application - what can a user see and what can they do.

Enough talk already

Let’s experiment ourselves and see how great Cypress is firsthand. Your Feedback Loop UI and Feedback Loop Api should already be up and running. Open cypress from within the FE repo using the command you set up as a script in package.json. Likely npm run cypress or npm run cypress:open. Select E2E Testing, then Chrome for your browser, then the Start E2E Testing in Chrome button.

We should have 3 spec files set up from the prework: login_spec.cy.js, dashboard_spec.cy.js and form_spec.cy.js. We could have chosen to use one giant file and test absolutely everything in there. But its probably more maintainable and intuitive to group up our related user flows into their own files.

Note

Notice that each of these describes actions tied to our data/server/network requests. When viewing feedback from coworkers, there are several different user flows. But they all involve GETTING feedback data from the back end.

Figuring out how to group user flows/stories can be tricky, and ultimately there are no hard-and-fast rules about how to do so. Over time, you’ll develop a sense of what to put together, just like how you are learning what to break out into a React component and what to leave as is. And, of course, these conventions change from team to team.

Inside login_spec.js, we’ll first write a dummy test to make sure things are hooked up correctly.

describe('Feedback Loop login flows', () => {
  it('Should confirm that true is equal to true', () => {
    expect(true).to.equal(true)
  });
});

Move over to the Cypress Test Runner and click on the login_spec.js and prepare to be amazed! Did it pass? Look at the Command Log and notice the assertion being made. Then try changing true to false and see if it fails.

User Flows

This is great and all but let’s think about what we actually need to test. Remember that Cypress is especially useful for testing user flows on our applications. List out a few user flows for the beginning of our application. (Hint: Refer to the user flows you brainstormed in the pre-work!)

Testing Our First User Flow

Now that we’ve identified some user flows, let’s get to testing (finally)! First, let’s focus on this user flow:

1a. As a user, I should be able to visit http://localhost:3000 and see a title & form displayed.

  • Write a test that asserts that a user can visit http://localhost:3000 using the visit command.
  • In the same it block, check to make sure that our site contains the correct title text.
  • Then, check to make sure our site can get the displayed form and that it contains the correct header text within the form.
  • Run your test to ensure no errors so far. Take note of any errors that you get in the Test Body of the Command Log.

1b. Adding onto the same flow, as a user, I can select the inputs, fill them out, as confirm that each input’s value matches what I typed.

  • In the same it block, get the email input field, type “leta@turing.io” into that field. This is the user action we’re simulating in Cypress. Now we need to assert against the results of that action.
  • Assert that the email input field should have the same value as whatever you typed into it.
  • Get the password input field, type “keane20” into that field.
  • Assert that the email input field should have the same value as whatever you typed into it.
  • Take note of any errors that you get in the Test Body of the Command Log.

This test might feel a bit unnecessary and overly simple - but it’s valuable. Thinking about controlled forms, what are we actually testing here?

Note - Your React app must be running in order for Cypress to work

If your test fails when trying to load your site, this might be because Cypress is actually trying to visit your page, but your server is not running. Make sure your React App server is running in a separate tab on your terminal! You do not need to have the API server running, though.

Before we continue, let’s add in the following block:

  beforeEach(() => {
    cy.visit('http://localhost:3000');
  });

Did You Know?

In the test runner, you can actually hit command + option + i to open up your DevTools! Instead of looking at your code, use your DevTools to find the necessary elements you need to query.

To add the React Dev Tools to your cypress browser window, take a look at this blog post.

2. User flow to test: I will receive an error message when I click the Submit button without filling out both inputs.

  • Write another test - in a new it block - that asserts an error message is displayed when the Submit button is clicked without filling both inputs.

Note - Never end a test on a “click”

Why? Because to test any user flow, we need to walk Cypress through simulating some user action. Then, we assert against whatever the user should see on the DOM as a result of that action. The click is the action, so its only getting us halfway there. We always need to add assertions after.

Here is a link to commonly used assertions in Cypress!

Writing tests involving network requests

User Story: As a user, I can correctly fill out the email and password inputs and click the Submit button and be directed to a different URL.

  • This builds off of what we have done previously, however we now want to test that when we log in successfully, our app takes us to a new url - http://localhost:3000/dashboard. It’s okay if the page doesn’t display all of the data on the next page, just assert that the url has updated for now.

Note

Upon filling out the form and submitting, you will likely run into a new error, Failed to fetch. This is because it is trying to access our API. In order to write a true end-to-end test, you could startup the server driving the application the same way a real user would. These are important around testing your application’s critical paths especially around happy paths.

There are some downsides however:

  • Because this is sending real responses, you normally would need to seed a database separate from your actual user’s info. (We don’t want to be making accidental changes to our user’s information and settings.)
  • This can slow the performance of your tests as a result of doing real network requests.
  • It is also more difficult to test edge cases.

For now (and throughout Mod 3), we will instead use stubbing and intercepting to control our network responses. Although both types of tests are important, stubbing is much more common and allows you to control the response body, status, and headers while also making your tests more performant.

Note that in our solution we are just intercepting the POST request for logging in and mocking out what the expected response would look like. Our dashboard is blank because we haven’t mocked out the other network requests; this is something we’ll test later on in our dashboard spec.

How can we tell which network requests have and have not been properly stubbed?

For now, all we are asserting is that our URL has updated to the page we expect to view when we are logged in. To thoroughly test this user flow, we’d also want to assert for all the elements and data we expect to see on the DOM.

Testing the Sad Path to a Network Request

User flow to test: I will receive an error message that my email and password don’t match if I submit incorrect email and password inputs.

  • Take what you learned from the exercise to stub a 401 response if a user fails to login. Assert that a new error message is displayed.

Handy should arguments cheatsheet

Take note of the different arguments passed through should when checking the values of an element on the page.

  • Input: .should('have.value', [some value]).
  • URL: .should('include', [some url])
  • Other DOM elements: .should('contain', [some text]').

Testing the Dashboard view

Let’s get a little more practice with intercepting network requests, by testing our Dashboard view.

In our dashboard_spec.js file, let’s pseudocode the user flows we should be testing.

  • After I login successfully, I should see the dashboard, complete with feedback from my teammates, as well as seeing teammates I have/haven’t left feedback for.

As you can see by digging through App.js and Dashboard.js, the way this code is constructed, there are no error messages when there is no appropriate user data. This is probably something we should fix in the future, but for now, we’ll only worry about testing the happy path.

Using Fixtures to Simplify Intercepts

Fixtures are a great way to manage and reuse static data in your tests. Let’s walk through using a fixture for one of the intercepts above. First, create a fixture file. In your cypress/fixtures directory, create a file named teammates.json and add the following content: First, create a fixture file. In your cypress/fixtures directory, create a file named teammates.json and add the following content:

{
  "teammates": [
    {
      "email": "hannah@turing.io",
      "id": 1,
      "image": "https://ca.slack-edge.com/T029P2S9M-UPE0QSWEQ-d4bebe6f4d88-512",
      "name": "Hannah Hudson",
      "delivered": false
    },
    {
      "email": "khalid@turing.io",
      "id": 3,
      "image": "https://ca.slack-edge.com/T029P2S9M-UDR1EJKFS-9351230a5443-512",
      "name": "Khalid Williams",
      "delivered": true
    }
  ]
}

Next, update your test to use this fixture:

cy.intercept(`http://localhost:3001/api/v1/users/2/teammates`, {
  statusCode: 200,
  fixture: 'teammates' // <<< Name of your fixture file 
});

Using body to pass in mock data

You can also pass in mock data using the body argument. This is when you have smaller mock data that you want to pass in. Choosing between using fixture or body is dependent on what you are working with, but both are good to know and use.

cy.intercept("GET", "http://localhost:3001/api/v1/users/11", {
  statusCode: 200,
  body: {
    id: 11,
    name: "Travis Rollins",
    image: "https://ca.slack-edge.com/T029P2S9M-U4R41TZD2-7661f06e8c71-512",
    email: "travis@turing.io",
    password: "rollins20",
  }
}

Exit Ticket

  • What is acceptance testing and how is it different from unit and integration tests?
  • What is Cypress and how is it different from other testing frameworks you’ve used in the past?
  • Should you include tests that utilize the API (end-to-end) or should you stub the network requests? Is there an argument for both?

Building From Here

Remember, this lesson and it’s activities are just an introduction to the world of testing with Cypress. We’ve only scratched the surface. In order to build your knowledge, fluency and skill to the level needed to succeed in Mod 3 and beyond, you will need to spend significant time in the Cypress documentation and getting your hands dirty with practice. In project-based learning, your projects will be your primary platform for learning. The feedback you get from that project work is what will help guide you towards further research and really help you level up your skill.

Resources

Lesson Search Results

Showing top 10 results