-
Notifications
You must be signed in to change notification settings - Fork 7
REACT TESTING BEST PRACTICES
A way to decrease test suite runtime is to increase the number of tests being run at the same time. Splitting up work typically happens at the file level, so at a minimum, a test file should not rely on any state from a previous test file.
Tests can also fail at any step. Each it
block is run regardless of previously run it
blocks. If one it
block relies on the state left by another it
block, cascading failures can happen that are very difficult to understand. Use before
to run once per describe block and beforeEach
to run once per nested it
block.
// bad
describe('Searches Page', () => {
it('should load the searches page', () => {
before(() => {
// visit searches page
})
})
it('should click the "My Favorites Tab"', () => {
// Click "My Favorites Tab"
})
it('should have the correct column names on the My Favorites Tab', () => {
// Assert column names
// whoops, if the previous test fails, this will also fail
})
it('should click the "All Searches Tab"', () => {
// Click "All Searches Tab"
})
it('should have the correct column names on the All Searches Tab', () => {
// Assert column names
// whoops, if the previous test fails, this will also fail
})
})
// good
describe('Searches Page', () => {
// run once for the entire "Searches Page" block
// we use `before` instead of `beforeEach` for speed because our tests can handle it only be run once
before(() => {
// visit searches page
})
describe('My Favorites Tab', () => {
// Run for every `it` block in the "My Favorites Tab" block
beforeEach(() => {
// Make sure we are in the right state for each `it` block
// Click the "All Searches Tab"
})
it('should have the correct column names', () => {
// Assert column names
})
})
})
Cypress uses Chai and also includes two extra matcher suites: chai-sinon and chai-jquery. The Cypress documentation lists them all here: https://docs.cypress.io/guides/references/assertions.html. A common problem is expecting something to be true which leads to unhelpful assertion failures. Bad assertions prevent using the cy.should
shorthand. Finding the right matcher does take more time, but the failure messages help narrow down what failed.
A good practice is to force an assertion to fail and see if the error message and the Cypress Log output is enough to know why. It is easiest to put a .only
on the it
block you're evaluating. This way the application will stop where a screenshot is normally taken and you're left to debug as if you were debugging a real failure. This practice will help you write better tests.
// bad
cy.get('body').should($body => {
expect($body.find('[data-testid=Tab]').length === 2).to.equal(true) // expected false to equal true
})
// better
cy.get('body').should($body => {
expect($body).to.have.descendants('[data-testid=Tab]') // expected '<body>' to have descendants '[data-testid=Tab]'
expect($body.find('[data-testid=Tab]')).to.have.length(2) // expected '[ <div[data-testid=Tab]>, 4 more... ]' to have a length of 2 but got 5
})
// best - we can use the shorthand
cy
.get('body')
.should('have.descendants', '[data-testid=Tab]') // expected '<body>' to have descendants '[data-testid=Tab]'
.find('[data-testid=Tab]')
.should('have.length', 2) // expected '[ <div[data-testid=Tab]>, 4 more... ]' to have a length of 2 but got 5
Every test you write will include selectors for elements. To save yourself a lot of headaches, you should write selectors that are resilient to changes.
Oftentimes we see users run into problems targeting their elements because:
- Your application may use dynamic classes or Id’s that change
- Your selectors break from development changes to CSS styles or JS behavior
Luckily, it is very easy to avoid both of these problems.
- Don’t target elements based on CSS attributes such as: id, class, tag
- Don’t target elements that may change their textContent
- Add data-* attributes to make it easy to target elements
Given a button that we want to interact with:
<button id="main" class="btn btn-large" data-cy="submit">Submit</button>
Targeting the element above by tag, class or id is very volatile and highly subject to change. You may swap out the element, you may refactor CSS and update ID’s, or you may add or remove classes that affect the style of the element. Instead, adding the data-cy attribute to the element gives us a targeted selector that’s only used for testing.
The “data-cy” attribute will not change from CSS style or JS behavioral changes, meaning it’s not coupled to the behavior or styling of an element.
Additionally, it makes it clear to everyone that this element is used directly by test code.
The button can be targeted with cy.get('[data-cy=submit]').click()
Other attributes that can be considered are:
- data-testid
- Data-test
If the content of the element changed, would you want the test to fail?
- If the answer is yes: then use “cy.contains()”
- If the answer is no: then use a data attribute.
Example: If we looked at the of our button again…
<button id="main" class="btn btn-large" data-cy="submit">Submit</button>
The question is: how important is the Submit
text content to your test? If the text changed from Submit
to Save
- would you want the test to fail?
If the answer is yes because the word Submit
is critical and should not be changed - then use cy.contains()
to target the element. This way, if it is changed, the test will fail.
If the answer is no because the text could be changed - then use cy.get()
with data attributes. Changing the text to Save would then not cause a test failure.