- 1. JavaScript testing #1. Explaining types of tests. Basics of unit testing with Jest
- 2. JavaScript testing #2. Introducing Enzyme and testing React components
- 3. JavaScript testing #3. Testing props, the mount function and snapshot tests.
- 4. JavaScript testing #4. Mocking API calls and simulating React components interactions
- 5. JavaScript testing #5. Testing hooks with react-hooks-testing-library and Redux
- 6. JavaScript testing #6. Introduction to End-to-End testing with Cypress
- 7. JavaScript testing #7. Diving deeper into commands and selectors in Cypress
- 8. JavaScript testing #8. Integrating Cypress with Cucumber and Gherkin
- 9. JavaScript testing #9. Replacing Enzyme with React Testing Library
- 10. JavaScript testing #10. Advanced mocking with Jest and React Testing Library
- 11. JavaScript testing #11. Spying on functions. Pitfalls of not resetting Jest mocks
- 12. JavaScript testing #12. Testing downloaded files and file inputs with Cypress
- 13. JavaScript testing #13. Mocking a REST API with the Mock Service Worker
- 14. JavaScript testing #14. Mocking WebSockets using the mock-socket library
In the previous part of this series, we’ve learned the basics of the Cypress framework. Although the above knowledge is enough to write our first tests, there is much more to explore. In this article, we dive a bit deeper into the mechanisms of Cypress. By doing so, we can avoid some troubles along the way and make our tests run smoother.
Asynchronous nature of Cypress commands
An example of a Cypress command is the cy.get function. So far, we’ve written quite a few selectors using it. We might think that under the hood, it works in the same way as regular JavaScript DOM selectors. That’s not exactly the case here.
1 2 |
cy.get('#search_input'); cy.get('#search_submit'); |
1 2 |
document.querySelector('#search_input'); document.querySelector('#search_submit'); |
We’ve already pointed out that Cypress retries the cy.get selector for us. Either it succeeds, or a timeout is reached, and the test fails. In fact, the cy.get is asynchronous!
We might think that the above operations can run in parallel if they are asynchronous. That’s not the case, though. Let’s inspect this test:
1 2 3 4 5 6 7 8 9 |
cy.visit('https://dev.wanago.io') cy.get('#search_input') .type('JavaScript') cy.get('#search_submit') .click(); cy.get('#search_term').should('contain.text', 'JavaScript'); |
Even though the above functions are asynchronous, they run sequentially, To better visualize this, we can think of executing Cypress commands as putting elements in a queue. The details of how they operate are managed under the hood. The above code results in the following actions:
- Opening a website
- and waiting for the load event to fire
- Looking for the input and filling it with value
- and retrying until it is found in DOM
- Looking for the button and clicking it
- while retrying until it is found and clickable
- Traversing the DOM tree looking for the term “JavaScript” in the search title
- performing the assertion until it succeeds
The waiting and retrying occur before the next step begins. As you can see, Cypress does quite a lot under the hood to keep our code simple and clean.
Because commands in Cypress are asynchronous and promised-based, their return value has the then function, among others. We can use it to interact with the result of the promise.
1 2 3 4 |
cy.get('#search_term') .then(searchTerm => { expect(searchTerm).to.contain.text('JavaScript'); }) |
The expect is a global function that acts in a similar way to the assertions that we defined as strings passed to the should function. Using it, we could manipulate the element in a more complicated way.
The should function also accepts a callback function in a very similar way to the then function.
1 2 3 |
cy.get('.radio-button').should(radioButton => { expect(radioButton).to.be.checked(); }) |
It differs from the then function because Cypress reruns the callback of the should function until no assertions throw inside it.
The and() function is an alias of should()
More on selectors
So far, the only selector function that we’ve used is cy.get. The Cypress framework offers more:
Find
The find function allows us to find a descendant of a particular DOM element. We need to chain it after another command that looks for DOM elements, for example, cy.get.
1 |
cy.get('#form').find('.radio-button'); |
Within
Works similar to find, but accepts a callback function.
1 2 3 4 5 |
cy.get('#form').within(form => { cy.get('#name_input').type('wanago.marcin@gmail.com'); cy.get('#password_input').type('strongPassword123'); form.submit(); }) |
An important note is that in the example above, cy.get('#name_input') and cy.get('#password_input') traverse the DOM tree only within the #form element.
Children
With the children function, we can get the children of a DOM element. If we don’t provide it with a selector, it gives us all children.
1 |
cy.get('.menu').children('.active'); |
1 |
cy.get('.menu').children().should('have.length', 4); |
Parents
It allows us to search through the ancestors of an element in a DOM tree, optionally filtered by a selector.
1 |
cy.get('.menu-element').parents('.menu'); |
Contains
The contains function makes possible looking for an element containing a particular text value.
1 |
cy.get('#registration_form').contains('Click here to register').click(); |
Eq
Using the eq function, we can get an element in an array that has a particular index.
1 |
cy.get('#ingredients_list').eq(1).should('contain', 'onion'); |
Good practices regarding selectors
A considerable problem when writing End-to-End tests is the issue of dealing with UI that is subject to change. When using Cypress, we write selectors all the time. To prevent them from breaking, we need to put some thought into it. If we rely too much on classes and tag names, things might get complicated when we update our interface. To prevent this from happening, we should write selectors that work even if there are some changes to the UI.
Fortunately, HTML5 was designed to be easily extended. The data-* attribute gives us an option to store additional information on regular HTML elements. Any attribute that starts with data- is a data attribute. We can make use of the above by adding the data-testid attribute.
Other popular naming conventions are data-cy and data-test
1 2 3 4 5 6 |
<button class="btn" data-testid="submit" > Submit </button> |
1 |
cy.get('[data-testid=submit]').click() |
By correctly inserting the above attributes, we can make sure that our selectors will not suffer when we make some changes to the UI.
Summary
In this article, we’ve dived more in-depth into the mechanisms of the Cypress framework. With a more extensive knowledge of how Cypress commands work, we can understand them better and make our code less prone to mistakes. Today we’ve also learned more about selectors and how we can use additional functions such as find and contains.
When would you advise against e2e?