End-to-End testing with Cypress

Now we have a good understanding of what End-to-End testing is and what it involves, let's now write some automated tests in Cypress to verify the areas of functionality the User cares about (i.e the User stories) are working as intended.

Learning Objectives

Install and run Cypress

Step 1

In your project directory, run npm install cypress --save-dev.

This will install all of the Cypress dependencies. It may take some time!

Step 2

Once done, run the command, ./node_modules/.bin/cypress open. If you have Cypress installed globally, you can just run cypress open.

This should open the Cypress GUI.

Setting up Cypress

Step 1

You'll notice that Cypress adds the following directories and a JSON configuration file:

- cypress
    - fixtures
    - integrations
    - plugins
    - support

- cypress.json

// other files

Step 2

In your cypress.json file, we're going to add the URL of your Live Server so we can tell Cypress which URL to load without repeating it for every test. We're also going to specify which folder to serve our tests from. In your file, add:

{
  "baseUrl": "http://127.0.0.1:5500/your_directory/index.html#/",
  "integrationFolder": "cypress/tests"
}

In our case, we're running the Live Server on port 5500 and want Cypress to load our index.html - the same page where Vue mounts itself on the app component. We've also added a configuration item to serve the tests from a /tests folder.

You might wonder why we've done this? Well, it's really a matter of preference more than anything but also shows the how useful the config can be in certain situations. Some developers may find it clearer that all tests live in the 'tests' folder, so let's roll with this!

Step 3

Delete the integrations folder and create an empty folder in the Cypress directory called tests.

All of the tests you add in the tests folder will combine to produce the E2E test coverage we want

Typical Cypress test structure

A good test should follow the pattern:

  1. Set up the application state (if applicable)
  2. Take an action
  3. Make an assertion about the resulting application state

If you've written unit tests in Jest or another JavaScript testing framework, then you'll be familiar with the callback style test structure:

describe("The thing you're testing", () => {
  it('should test an aspect of the thing', () => {
    cy.visit('/'); // take an action
    cy.get('some-element').should('have.text', 'some text'); // make an assertion
  });
});

A TDD approach to our first test

Our products really should show an "Out of stock" notice if the stock level is equal to zero and we shouldn't show the Add to cart button if the product is out of stock. Let's write a failing test for this first, then add the functionality to make it pass.

Step 1

Cypress recommends adding data attributes to the elements we want to capture. This ensures that even if IDs and classes change, your tests will still run because they are powered by data attributes.

In your main.js file, modify your Product component by adding data-cy="product" to the main div:

// main.js

const Product = Vue.component('Product', {
  template: `
    <div class="product" data-cy="product">

    // other fields omitted
   `,
});

Step 2

Now create new file in your tests dir called product.spec.js and add:

// product.spec.js

describe('Product', () => {
  beforeEach(() => {
    cy.visit('/'); // 1
  });

  it('should find an out of stock notice but no add to cart button', () => {
    cy.get('[data-cy=product]') // 2
      .eq(0) // 3
      .should('contain', 'Out of stock :(') // 4
      .and('not.contain', 'Add to cart'); // 5
  });
});

Here, we are:

  1. Using the beforeEach method to visit our baseUrl in the cypress.json file before each test.

  2. Getting the product divs using the attribute [data-cy=product]

  3. Filtering by the first one

  4. Making the assertion that somewhere in this block, the text 'Out of stock :(' will be found

  5. Making the assertion that the text 'Add to cart' will not be found

Step 3

Save your code and the tests should re-run. Of course, they will fail, but we'll fix that next.

Fixing our broken test

Now we need to turn our attention to our Vue.js code to add an out of stock notice if the stock level is equal to zero and to only show the the 'Add to cart' button if the stock level is greater than zero. We can achieve this by using Vue's v-if tag.

In your Product component, add:

<p v-if="product.stockLevel === 0">Out of stock :(</p>

And on the Add to cart button:

<button v-on:click="addToCart(product)" v-if="product.stockLevel > 0">
  {{ product.addedToCart ? "Remove from cart" : "Add to cart" }}
</button>

Now if you re-run your Cypress test, it should pass!

A note on "flaky tests"

"A flaky test is a test that fails to produce a consistent result each time it is run"

Now, thinking about the test that we've just added, would you say it is flaky?

We're very much writing this app in a dev environment, but if it were a live project, having to change the first product's stock level to zero just for this test to pass would not be a good idea. Instead, we would launch the app in a testing/staging environment and use test data to ensure our tests produce consistent results.

In this instance, since we're in practice mode, you can simply change the stock level back to a positive number and skip the test:

it.skip('should find an out of stock notice but no add to cart button', () => {
  // test code omitted
});

Assignment - Coding challenge

You'll remember that the footer address and copyright notices are returned by computed methods on the footer component. It makes sense to test these to ensure that they work consistently.

Your challenge is to add a footer.spec.js test file and test the output is what you expect.