Skip to content

REACT TESTING BEST PRACTICES

ipheghe edited this page Jun 22, 2018 · 2 revisions

Each test must be runnable in complete isolation

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
    })
  })
})

Assertions should be obvious

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

Selecting Elements

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

Text Content

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.