Test Driven-Development with E2E Tests

Test Driven-Development with E2E Tests

Don't write the entire flow, do tiny steps.

Introduction

I write my tests using Cypress. If you know me, you know I love TDD. In this article, I will share my experience doing TDD with Cypress.

Cypress

I love Cypress. It is the best E2E testing framework. I use it in combination with Testing Library in order to use accessible queries which don't just increase the confidence because you aren't testing implementation details, but the experience is a lot better.

I love how the watch mode is fast and responsive. The debugging experience is also fantastic, and the recording, being able to view the entire test and stop at points to see how things looked at a specific time during the test.

Cypress is phenomenal.

The wrong way

Doing TDD with Cypress can be hard. Back in the day, I used to write the entire test suite before writing the implementation of the feature, it was a pain. It was a pain for a few reasons:

  • Writing the entire test is hard when the flow isn't small which is usually the case with E2E Tests. You may assert something wrong and you feel nervous when writing the test.
  • If you have written the entire flow, when implementing the feature, you may write more code than necessary, and it is scary being in a position where you need to touch dozen of files and write loads of code just to get that huge test to pass.

This was a pain. I have been thinking of how to do it differently, more effectively, and I remembered, it is all about tiny steps.

The right way

I don't write the entire test now. I write a bit of it, make sure it fails, and then I go and write the code to make the test pass. Lastly of course, once the test has passed, I look to if I can refactor any code.

  1. Write a small piece of the test flow.
  2. Write the code to make the failing test pass.
  3. Refactor the code.

I love doing it this way, it has been such a joyful experience. I feel less nervous and anxious when writing the implementation of the feature.

Example

An example always helps to clarify things.

Below we have an entire flow where a user signs up.

it('Should be able to sign up', () => {
  cy.visit('/')

  cy.findByRole('link', { name: 'Kamuy' }).should('be.visible')
  cy.findByRole('heading', { name: 'Sign In' }).should('be.visible')

  cy.findByText(
    "If your account doesn’t exist, we'll create one, otherwise you just sign in."
  ).should('be.visible')

  cy.login(newUser)

  cy.findByRole('button', { name: 'Sign In' }).click()

  cy.findByRole('status')
    .findByText(SIGNED_UP_SUCCESS_MESSAGE)
    .should('be.visible')
})

There is quite an amount of code we would have to write to get this to pass, let's break it down into chunks, and do tiny steps!

it('Should be able to sign up', () => {
  cy.visit('/')

  // Assert the UI
  cy.findByRole('link', { name: 'Kamuy' }).should('be.visible')
  cy.findByRole('heading', { name: 'Sign In' }).should('be.visible')
  cy.findByText(
    "If your account doesn’t exist, we'll create one, otherwise you just sign in."
  ).should('be.visible')

  // Login
  cy.findByLabelText('Email').type(user.email)
  cy.findByLabelText('Username').type(user.username)
  cy.findByLabelText('Password').type(user.password)
  cy.findByRole('button', { name: 'Sign In' }).click()

  // Toast message
  cy.findByRole('status')
    .findByText(SIGNED_UP_SUCCESS_MESSAGE)
    .should('be.visible')
})

Instead of writing the entire flow, we can begin with the first part which asserts the UI on the home page.

it('Should be able to sign up', () => {
  cy.visit('/')

  // Assert the UI
  cy.findByRole('link', { name: 'Kamuy' }).should('be.visible')
  cy.findByRole('heading', { name: 'Sign In' }).should('be.visible')
  cy.findByText(
    "If your account doesn’t exist, we'll create one, otherwise you just sign in."
  ).should('be.visible')
})

We begin by writing the implementation for this part of the test, and getting it to work. It is much easier than having so much in your head and trying to figure things out, especially since we're dealing with authentication above.

Don't write the entire test, write a part of it followed by the implementation and a refactoring, then proceed to the next part of the test.

Conclusion

Doing TDD when writing E2E Tests can be hard if you do it the wrong way. By breaking it down into multiple steps, everything just becomes much better.