Difference between brittle and non-brittle tests in React JS

We are going to write two tests - one of the test will be brittle (it will test the implementation) and other test will be non-brittle (it will test the functionality).

App.js

import React, { Component } from 'react';

export default class Brittle extends Component{
    state = {
        number: 0
    }

    incrementNumber = () => {
        const { number } = this.state;
        this.setState({
            number: number+1
        })
    }

    render(){
        const { number } = this.state;
        return(
            <div>
                <div data-testid="increment-number">
                    {number}
                </div>
                <button data-testid="increment-button" onClick={this.incrementNumber}>
                    Increment Number
                </button>
            </div>
        )
    }
}

Here the functionality of component is that when the user clicks on button, the number is updated on screen.

The implementation is done using the state, calling the incrementNumber function.

Here is an example of bad test, because if the developer changes the name of incrementNumber function to incrementNo, then the test is going to fail (Even though the functionality is not broken, the test fail unnecessarily).

it('should increment number - implementation test', () => {
    const wrapper = shallow(<Brittle />);
    wrapper.instance().incrementNumber();
    expect(wrapper.state('number')).toEqual(1);
})

Following is the example of good test, which is independant of implementation.

it('should increment number - functionality test', () => {
    const wrapper = shallow(<Brittle />);
    expect(wrapper.find('[data-testid="increment-number"]').text()).toEqual('0')
    wrapper.find('[data-testid="increment-button"]').simulate('click');
    expect(wrapper.find('[data-testid="increment-number"]').text()).toEqual('1')
})

Let us test our assumption. We are going to change the name of function from incrementNumber to incrementNo. We are also going to change the name of state from number to no.

Our modified App.js class is as follows

import React, { Component } from 'react';

export default class Brittle extends Component{
    state = {
        no: 0
    }

    incrementNo = () => {
        const { no } = this.state;
        this.setState({
            no: no+1
        })
    }

    render(){
        const { no } = this.state;
        return(
            <div>
                <div data-testid="increment-number">
                    {no}
                </div>
                <button data-testid="increment-button" onClick={this.incrementNo}>
                    Increment Number
                </button>
            </div>
        )
    }
}

Here are the result. The brittle test (first test) failed, while the second test passed.

Result for the failed brittle test

FAIL - should increment number - implementation test

    TypeError: wrapper.instance(...).incrementNumber is not a function

      16 |     it('should increment number - implementation test', () => {
      17 |         const wrapper = shallow(<Brittle />);
    > 18 |         wrapper.instance().incrementNumber();
         |                            ^
      19 |         expect(wrapper.state('number')).toEqual(1);
      20 |     })


The brittle test is not able to find the incrementNumber function, as we have renamed it.

Conclusion

So you have understood how to write tests in a way which do not break when a future developer changes the code without changing the functionality of component. Its going to save your time writing unnecessary tests, and also save the future developer's time re-writing the failed tests.