How to end-to-end test with Cypress

Getting started with end-to-end testing ๐Ÿ‘

ยท

7 min read

What is end-to-end testing?

So what is end-to-end testing? We have two ends in a Web application: the front-end and the back-end. With unit-testing, we test the code for the front-end or the back-end. We don't test how the application behaves in the browser or how both ends work together.

Writing unit tests is good, in my opinion!

But testing how the front-end and back-end work together is equally important.

We test if the front-end works well with the back-end with end-to-end testing. So automatically filling forms, clicking buttons, navigate through the pages should be tested by end-to-end tests.

Running the end-to-end tests will validate if your front-end is handling the data correctly received from the back-end.

divider-byrayray.png

What is Cypress?

There are a lot of end-to-end testing toolkits, but one of the most popular and fastest toolkits is Cypress.

Cypress offers a way of writing end-to-end tests with JavaScript and the test runner. Next to that, they offer a way to save screenshots and video's when a test fails. What most organizations love, it's open-source, and that is great.

divider-byrayray.png

How do we use Cypress with end-to-end testing?

There is a great tutorial in the Cypress documentation to get you started on writing end-to-end tests.

The configuration

At the root of the project, there is a cypress.json where you can change some default configurations. In our project, it looks like this:

{
    "testFiles": "**/*.e2e.test.js",
    "chromeWebSecurity": false
}

In the testFiles property, we tell Cypress to look for files that include e2e.test.js in the name. You can configure Cypress with TypeScript, but in this case, I think there is no added value to using it. It requires an additional transpilation step that takes longer.

divider-byrayray.png

Where to store the test files?

With unit-testing, it's very common to store the tests in the component folders. In this case, Cypress takes the cypress/integration folder as the root to look for the files. So we have a structure based on the application itself, in which we store the end-to-end testing files.

divider-byrayray.png

Minimal requirements of an end-to-end test

Every test has at least a describe() function within that at least one it() function. This works similarly to writing unit tests.

describe('My First Test', () => {
  it('Does not do much!', () => {
    expect(true).to.equal(true)
  })
})

This example looks like a unit test. So we need to make use of the Cypress Library. You can use that by using cy like the example below in which we visit https://example.cypress.io.

describe('My First Test', () => {
  it('Visits the Kitchen Sink', () => {
    cy.visit('https://example.cypress.io')
  })
})

You can see the real browser performing this action by running npm run e2e:open.

Our scripts in the package.json look like this.

{
//...
"e2e:run": "npx cypress run --config-file cypress.json",
"e2e:open": "npx cypress open --config-file cypress.json",
"e2e:open:edge": "npx cypress open --browser edge --config-file cypress.json",
"e2e:open:firefox": "npx cypress open --browser firefox --config-file cypress.json"
//...
}

Cypress will start its application, and there you can select the end-to-end test you want to run. By default, it will open the Chrome browser, but there is also an option to run it in Firefox and Edge.

Cypress offers a complete library, and you can check the documentation on what they all include.

divider-byrayray.png

What should you test in an end-to-end test?

We will mainly test things that appear or change in the browser with end-to-end tests. Like a simple scenario: We visit page x and check if the h1 contains the text "This is an awesome title".

describe('My First Test', () => {
  it('Visits the Kitchen Sink', () => {
    cy.visit('https://example.cypress.io');
    cy.get('h1').should('contain.text', 'This is an awesome title');
  })
})

In this example, every method you call from the cy library is part of testing. Like cy.visit() will perform the action, but if there is no valid page on that URL, it will fail your test. So with every action you perform, you commit an assertion.

In Cypress, there are two different types of assertions. First "Implicit Subjects", in this case you use should() or and(). Second, "Explicit Subjects", in this case, you use expect. Read more on this in the Cypress documentation.

When calling cy.get('h1') it will search for the <h1> tag. When it finds that element, your test will continue. It will fail the test if it doesn't find the element in a few milliseconds. After the get method, you can chain a lot of other methods, like:

  • .contains() expects the element with content to eventually exist in the DOM.
  • .find() also expects the element to exist in the DOM eventually.
  • .type() expects the element to eventually be in a typeable state.
  • .click() expects the element to eventually be in an actionable state. .its() expects to eventually find a property on the current subject.

So what should we test in end-to-end tests?

  1. If an element has a certain class cy.get('element.selector').should('have.class', 'ng-valid'); or doesn't have a certain class cy.get('element.selector').should('not.have.class', 'ng-valid');.
  2. If a list has 3 child elements cy.get('ul > li').should('have.length', 3);.
  3. Check if an input field or textarea has a certain value cy.get('input[name="firstName"]').type('Santa Claus').should('have.value', 'Santa Claus');.

Check this list of examples what you can use in the should method.

Extra example:

Add the context of a parent element to perform assertions, which comes in handy with forms.

<form>
  <input name="email" type="email" />
  <input name="password" type="password" />
  <button type="submit">Login</button>
</form>
describe('My First Test', () => {
  it('Visits the Kitchen Sink', () => {
    cy.visit('https://example.cypress.io');
    cy.get('form').within(($form) => {
        // you have access to the found form via
        // the jQuery object $form if you need it

        // cy.get() will only search for elements within form,
        // not within the entire document
        cy.get('input[name="email"]').type('john.doe@email.com')
        cy.get('input[name="password"]').type('password')
        cy.root().submit()
    })
  })
})

Check for more examples of the within() method in the Cypress documentation.

divider-byrayray.png

Re-usable functionality

In the case of re-useable actions that you perform over-and-over-again, you can write simple functions that you can put in a lib folder. But Cypress offers a more friendly way of doing this. This is called "custom commands"; you can find a lot of examples in the documentation.

But one of the best examples is writing the command for the login. You need to add that code in the cypress/support/commands.js file.

Cypress.Commands.add('login', (userType, options = {}) => {
  // this is an example of skipping your UI and logging in programmatically

  // setup some basic types
  // and user properties
  const types = {
    admin: {
      name: 'Jane Lane',
      admin: true,
    },
    user: {
      name: 'Jim Bob',
      admin: false,
    },
  }

  // grab the user
  const user = types[userType]

  // create the user first in the DB
  cy.request({
    url: '/seed/users', // assuming you've exposed a seeds route
    method: 'POST',
    body: user,
  })
    .its('body')
    .then((body) => {
      // assuming the server sends back the user details
      // including a randomly generated password
      //
      // we can now login as this newly created user
      cy.request({
        url: '/login',
        method: 'POST',
        body: {
          email: body.email,
          password: body.password,
        },
      })
    })
})

Conclusion

Now it's up to you to write great end-to-end tests with Cypress! Perfect tests don't exist, but if you optimize them every time a bit, it will become your source of truth. The end-to-end tests are great for developers to run after building a new feature or changing an existing one because it's impossible to keep all the testing scenarios in your brain.

Good luck and have fun with writing valuable end-to-end tests ๐Ÿ‘

Happy Coding ๐Ÿš€

Thanks!

hashnode-footer.png I hope you learned something new or are inspired to create something new after reading this story! ๐Ÿค— If so, consider subscribing via email (scroll to the top of this page) or follow me here on Hashnode.

Did you know that you can create a Developer blog like this one, yourself? It's entirely for free. ๐Ÿ‘๐Ÿ’ฐ๐ŸŽ‰๐Ÿฅณ๐Ÿ”ฅ

If I left you with questions or something to say as a response, scroll down and type me a message. Please send me a DM on Twitter @DevByRayRay when you want to keep it private. My DM's are always open ๐Ÿ˜

Did you find this article valuable?

Support Dev By RayRay by becoming a sponsor. Any amount is appreciated!

ย