DEV Community

Cover image for React Testing with Airbnb's Enzyme, Jest and Babel
Mark A
Mark A

Posted on • Updated on

React Testing with Airbnb's Enzyme, Jest and Babel

Hi Guys Good Day!

ok, guys first We gonna configure Jest, Enzyme from the ground up. So you can know what modules or packages we're gonna use.

Do this in the command line on your desktop.

  md testing-with-enzyme && cd testing-with-enzyme
Enter fullscreen mode Exit fullscreen mode

md testing-with-enzyme - Makes a directory with a name of testing-with-enzyme
&& - runs the second command if the first command does not throw an error
cd testing-with-enzyme - Changes the current directory to testing-with-enzyme

  npm init --y && npm i -D @babel/preset-env @babel/preset-react 
  @babel/plugin-proposal-class-properties @types/jest jest
  enzyme enzyme-adapter-react-16 && npm i -P react react-dom
Enter fullscreen mode Exit fullscreen mode

ok, I'm not gonna explain all these packages but we're gonna all of these packages to work with enzyme and jest.

 type nul > babel.config.js && type nul > jest.config.js && md Tests && md components
Enter fullscreen mode Exit fullscreen mode

type nul for Windows OS. touch for UNIX Systems.

Our babel.config.js file.

module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react'],
  plugins: ['@babel/plugin-proposal-class-properties']
}
Enter fullscreen mode Exit fullscreen mode

Our jest.config.js file.

module.exports = {
  rootDir: '.',
  displayName: {
    name: 'enzyme-setup',
    color: 'blue'
  },
  runner: 'jest-runner',
  verbose: true,
  errorOnDeprecated: true,
  roots: ['./Tests'],
  moduleFileExtensions: ['js', 'jsx'],
  setupFilesAfterEnv: ['<rootDir>Tests/setupTest.js']
}
Enter fullscreen mode Exit fullscreen mode

Inside our Tests folder make a setupTest.js file.

import { configure } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'

configure({
  adapter: new Adapter()
})
Enter fullscreen mode Exit fullscreen mode

Our package.json file.
package.json

Inside the components folder make 4 files.

  type nul > App.js && type nul > Form.js && type nul > Header.js && type nul > List.js
Enter fullscreen mode Exit fullscreen mode

Our Header.js file.

import React from 'react'

export default function Header({ message, handleToggleTheme, theme }) {
  return (
    <div className="header">
      <h1>{message}</h1>
      <button className="right" onClick={handleToggleTheme}>
        <i className={theme}></i>
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Our Form.js file.

import React from 'react'

export default function Form({ handleChange, value, handleClick }) {
  return (
    <div className="form">
      <input
        className="form-control"
        type="text"
        onChange={handleChange}
        value={value}
      />
      <button className="btn" onClick={handleClick}>
        Submit
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Our List.js file.

import React from 'react'

export default function List({ items }) {
  return (
    <ul className="list">
      {items.map(item => (
        <li className="list-item" key={item}>{item}</li>
      ))}
    </ul>
  )
}
Enter fullscreen mode Exit fullscreen mode

Our App.js file.

import React, { Component } from 'react'
import Header from './Header'
import List from './List'
import Form from './Form'

export default class App extends Component {

  state = {
    listItem: '',
    items: [],
    isDarkTheme: false
  }



  handleChange = ({ target: { value } }) => {
    this.setState({
      listItem: value
    })
  }

  handleClick = () => {
    this.setState({
      items: [...this.state.items, this.state.listItem],
      listItem: ''
    })
  }


  handleToggleTheme = () => {
    this.setState({
      isDarkTheme: !this.state.isDarkTheme
    })
  }

  render() {
    const theme = this.state.isDarkTheme ? 'dark' : 'light'
    return (
      <div className={theme}>
        <Header
          theme={theme}
          message={this.props.message}
          handleToggleTheme={this.state.handleToggleTheme}
        />
        <Form
          handleChange={this.state.handleChange}
          value={this.state.listItem}
          handleClick={this.state.handleClick}
        />
        <List items={this.state.items} />
      </div>
    )
  }

}

App.defaultProps = {
  message: 'Hello World'
}
Enter fullscreen mode Exit fullscreen mode

Inside the Tests folder make an index.test.js file.

import React from 'react'
import App from '../components/App'
import Header from '../components/Header'
import Form from '../components/Form'
import List from '../components/List'
import { shallow, mount } from 'enzyme'

describe('Test App component', () => {
  let wrapper;

  beforeAll(() => {
    wrapper = shallow(<App />)
  })

  it('should not return an error', () => {
    expect(wrapper).toMatchSnapshot()
    console.log(wrapper.debug())
  })
})
Enter fullscreen mode Exit fullscreen mode

Then in your terminal run this command

 npm t
Enter fullscreen mode Exit fullscreen mode

If it does not throw an error and it passed then your good to go.

What's the difference between shallow rendering and full mount rendering?

There's this very useful method debug that both shallow and mount provides us.

Try updating our index.test.js file to look like this.

import React from 'react'
import App from '../components/App'
import Header from '../components/Header'
import Form from '../components/Form'
import List from '../components/List'
import { shallow, mount } from 'enzyme'

describe('Test App component', () => {
  let shallowWrapper, mountWrapper;

  beforeAll(() => {
    shallowWrapper = shallow(<App />)
    mountWrapper = mount(<App />)

    console.log(shallowWrapper)
    console.log(mountWrapper)
  })

})
Enter fullscreen mode Exit fullscreen mode

Structure using the debug method.

The first console.log looks like this.

 console.log Tests/index.test.js:12
    <div className="light">
      <Header theme="light" message="Hello World" handleToggleTheme={[Function]} 
        />
      <Form handleChange={[Function]} value="" handleClick={[Function]} />
      <List items={{...}} />
    </div>
Enter fullscreen mode Exit fullscreen mode

The second console.log looks like this.

  console.log Tests/index.test.js:13
    <App message="Hello World">
      <div className="light">
        <Header theme="light" message="Hello World" handleToggleTheme={[Function]}>
          <div className="header">
            <h1>
              Hello World
            </h1>
            <button className="right" onClick={[Function]}>
              <i className="light" />
            </button>
          </div>
        </Header>
        <Form handleChange={[Function]} value="" handleClick={[Function]}>
          <div className="form">
            <input className="form-control" type="text" onChange={[Function]} value="" />
            <button className="btn" onClick={[Function]} />
          </div>
        </Form>
        <List items={{...}}>
          <ul className="list" />
        </List>
      </div>
    </App>
Enter fullscreen mode Exit fullscreen mode

The debug method basically gives us the structure of our component. When we use it on shallow it does not give us the full structure of our component we don't see the JSX structure of our Header,Form, and List component but when we use it on mount it gives us the full structure of our component down to every JSX element that our child components used.

Useful methods that Enzyme provides us.

at(index : number)

Returns a wrapper element based on the index is given.

The difference between using shallow and mount on our App component.

  it('should have an "App" component "at" index of 0', () => {
    let wrapper = shallow(<App />);
    expect(wrapper.at(0).type()).toBe(App);
  });

  it('should return an App', () => {
    let wrapper = mount(<App />);
    expect(wrapper.at(0).type()).toBe(App)
  });

Enter fullscreen mode Exit fullscreen mode

The first test failed but the second test passed the reason to this is that the type of element at index 0 in our shallowed component is div, not App but in our mountend component is App refer to the Structure Section changing the App to div in the shallowed test will make the test passed.

childAt(index : number)

Returns a new wrapper of the child at the specified index.

  it('should have a child component of type "Header" at "index" of 0', () => {
    let wrapper = shallow(<App />);
    expect(wrapper.childAt(0).type()).toBe(Header);
  });

  it('should have a child element of type "div" at "index" of 0', () => {
    let wrapper = mount(<App />);
    expect(wrapper.childAt(0).type()).toBe('div')
  });
Enter fullscreen mode Exit fullscreen mode

Base on the Structure of our shallowed App the first child should be Header and in our mounted App the first child should be div. These two tests should passed.

find(selector : EnzymeSelector)

Basically finds every node that matches the given selector.

Selectors.

  • find('div') = finds every 'div' element on the current wrapper.
  • find('div.something') = finds every 'div' element with a class of 'something' on the current wrapper.
  • find('div[title="okinawa"]) = finds every 'div' element with an attribute of "title" with a value of "okinawa".

  • find('#okinawa') = find every element with an id of "okinawa".

  • find('.okinawa') = finds every element with a class of "okinawa".

  • find('div#okinawa > span') = finds every 'span' element that is the
    direct child of a "div" with an id of "okinawa"

  • find('div.okinawa + span') = finds every 'span' element that is placed after a "div" element with a class of "okinawa"

  • find('div.okinawa span') = finds every 'span' element that is inside a "div" element with a class of "okinawa"

  • find(SomeComponent) = finds every element with a contrustor of "SomeComponent"

   function App({ children }){
     return (
       <div>
           {children}
        </div>
     )
   }   


   function SomeComponent(){
      return (
        <div>
         <h1>
            Hi!
         </h1>
        </div>
      )
   }


   it('should have length of "1" when finding "SomeComponent" comp', () => {
    const wrapper = shallow(<App>
         <SomeComponent />
     </App>
     )
    expect(wrapper.find(SomeComponent)).toHaveLength(1);
   });

Enter fullscreen mode Exit fullscreen mode

You can find all the valid selectors here.

closest(selector : EnzymeSelector)

finds the closest parent that matches the selector. It traverses every node up starting with itself.


  it('should have an h1 with a text of "Hello World"', () => {
    let wrapper = shallow(<App />);
    expect(wrapper.find(Header).closest('div.light')).toHaveLength(1);
  });


  it('should have a parent element of "div" with a class of "light"', () => {
    let wrapper = mount(<App />);
    expect(wrapper.find(Header).closest('div.light')).toHaveLength(1);
  })

Enter fullscreen mode Exit fullscreen mode

These two tests should pass.

contains(node : node | nodes[])

Tests if the containing wrapper has a matching child or children.

   it('should have a node of <Header /> and the right props', () => {
    let wrapper = shallow(<App />);
    expect(wrapper.contains(
      <Header theme="light" message="Hello World" handleToggleTheme= 
           {wrapper.instance().handleToggleTheme} />
    )).toBeTruthy();
  });



  it('should contain these two nodes', () => {
    const wrapper = mount(<App />);
    expect(wrapper.contains([
      <h1>Hi</h1>,
      <button className="right" onClick={wrapper.instance().handleToggleTheme}>
        <i className="light" />
      </button>
    ])).toBeTruthy();
  })

Enter fullscreen mode Exit fullscreen mode

We're using the instance() method to get the reference of the handleToggleTheme function out of that component instance. More on the instance method later. These tests should pass.

containsAllMatchingElements(nodes: nodes[])

Must match all the nodes on the current wrapper.

 it('should have these two nodes when shallow mounting', () => {
   let wrapper = shallow(<App />);
    wrapper.setState({ listItem: '1' })
    expect(wrapper.containsAllMatchingElements(
      [
        <Form handleChange={wrapper.instance().handleChange} value="1" handleClick={wrapper.instance().handleClick} />,
        <Header theme="light" message="Hello World" handleToggleTheme={wrapper.instance().handleToggleTheme} />
      ]
    )).toBeTruthy();
  });

  it('should have these two nodes when mounting', () => {
    let wrapper = mount(<App />);
    expect(wrapper.containsAllMatchingElements([
      <h1>Hi</h1>,
      <button className="right" onClick={wrapper.instance().handleToggleTheme}>
        <i className="light" />
      </button>
    ])).toBeTruthy();
  })


Enter fullscreen mode Exit fullscreen mode

We're using setState to update the value of a property in our state. It works the same as React's setState. this.setState({property: newValue}). These tests should pass.

containsAnyMatchingElements(nodes: nodes[])

Must match at least one of the nodes on the current wrapper.

  it('should this Form with the right props', () => {
    expect(wrapper.containsAnyMatchingElements(
      [
        <Form handleChange={wrapper.instance().handleChange} value="1" handleClick={wrapper.instance().handleClick} />,
      ]
    )).toBeTruthy();
  });

  it('should return true because the "i" element is right while "div" element is not the right structure', () =>{
    expect(wrapper.containsAnyMatchingElements([
      <div className="form">
      </div>,
      <i className="light" />
    ])).toBeTruthy();
  });

Enter fullscreen mode Exit fullscreen mode

You're wondering why we have a value of "1" in the shallowed part that's because we used setState on the previous section and updated our listItem to have a value of 1. These tests should pass.

first()

Behaves like at(0) refer.

hasClass(class:string)

Tests if the current node has prop of className and checks the value.


   it('should have a class of "light"', () => {
    let wrapper = shallow(<App />);
    expect(wrapper.hasClass('light')).toBeTruthy();
   });

   it('should have a class of "form-control"', () => 
     wrapper = mount(<App />);  
    {
    expect(wrapper.find(Form).find('#form').childAt(0).hasClass('form-control')).toBeTruthy();
  })

Enter fullscreen mode Exit fullscreen mode

These tests should pass.

html()

returns the raw html string of the current wrapper.

   it('should return the correct string', () => {
    let wrapper = shallow(<App >);
    expect(wrapper.childAt(2).html()).toBe('<ul class="list"></ul>')
  });

  it('should have an element with an id of "form"', () => {
    let wrapper = mount(<App >);
    wrapper.setProps({ message: 'Hi' });
    expect(wrapper.find('h1').html()).toBe('<h1>Hi</h1>')
  })

Enter fullscreen mode Exit fullscreen mode

These tests should pass too.

instance()

returns the current class instance of the current wrapper it returns null when used on a functional component. the instance method can only be used on the root node.


   it('should be an instance of App', () => {
    let wrapper = shallow(<App />);
    expect(wrapper.instance()).toBeInstanceOf(App);
  });


  it('should be an instance of App', () => {
    let wrapper = mount(<App />);
    expect(wrapper.instance()).toBeInstanceOf(App);
  });
Enter fullscreen mode Exit fullscreen mode

These tests should pass.

invoke(functionPropName)(..arguments)


  it('should have a prop of "value" with a value of "12344"', () => {
    let wrapper = shallow(<App />);
    wrapper.find(Form).invoke('handleChange')({ target: { value: '12344' } });
    expect(wrapper.find(Form).prop('value')).toBe('12344');
  });


  it('should truthy value of prop "isDarkTheme"', () => {
    let wrapper = mount(<App />);
    wrapper.find(Header).invoke('handleToggleTheme')()
    expect(wrapper.state('isDarkTheme')).toBeTruthy();
  })

Enter fullscreen mode Exit fullscreen mode

These tests should pass too. I think you're wondering I'm passing an object with a target property which has a value of an object with another property of value because my handleChange function looks like this

   handleChange = ({ target: { value } }) => {
    this.setState({
      listItem: value
    })
  }
Enter fullscreen mode Exit fullscreen mode

is(selector: EnzymeSelector)

Checks if the selector matches the current wrapper.


  it('should return false when checking with ".is"', () => {
    let wrapper = shallow(<App />);
    expect(wrapper.find(List).find('ul').is('.list')).toBeFalsy();
  });


  it('should return true when checking with ".is"', () => {
    let wrapper = mount(<App />);
    expect(wrapper.find(List).find('ul').is('.list')).toBeTruthy();
  });
Enter fullscreen mode Exit fullscreen mode

The reason that the first test failed and thrown an error because of the reason
that our element structure when using shallow looks like this

    <div className="light">
      <Header theme="light" message="Hello World" handleToggleTheme={[Function]} 
        />
      <Form handleChange={[Function]} value="" handleClick={[Function]} />
      <List items={{...}} />
    </div>
Enter fullscreen mode Exit fullscreen mode

It does not render the ul element but when we use it on mount it works.

isEmptyRender()

returns true if the current wrapper returns null or false.


  it('should not be falsy because "App" does not return neither null or false', () => {
    let wrapper = shallow(<App />);
    expect(wrapper.isEmptyRender()).toBeFalsy();
  });

  it('should return "Nothing" literally', () => {
    class Nothing extends React.Component {
      render() {
        return (
          null
        )
      }
    }
    let wrapper = mount(<Nothing />);
    expect(wrapper.isEmptyRender()).toBeTruthy();
  });

Enter fullscreen mode Exit fullscreen mode

These tests should pass. The second test pass due to the reason we returned null on the render method.

key()

returns the key value of the current wrapper.

  it('should have a prop of items with a length of 2 and a key value of "japan"', () => {
    let wrapper = mount(<Form />);
    let form = wrapper.find(Form);
    form.invoke('handleChange')({ target: { value: 'okinawa' } });
    form.invoke('handleClick')();
    form.invoke('handleChange')({ target: { value: 'japan' } });
    form.invoke('handleClick')();

    expect(wrapper.find(List).prop('items')).toHaveLength(2);
    expect(wrapper.find(List).find('ul').childAt(1).key()).toBe('japan');
  });


Enter fullscreen mode Exit fullscreen mode

last()

returns the last node base on the current selected wrapper.


  it('should return the last child type which is "List"', () => {
    let wrapper = shallow(<App />);
    expect(wrapper.children().last().type()).toBe(List);
  });

  it('should return the last child type which is "div"', () => {
    let wrapper = mount(<App />)
    expect(wrapper.children().last().type()).toBe('div');
  });

Enter fullscreen mode Exit fullscreen mode

name()

returns the "name" of current wrapper.

   it('should return a name with a value of "div"', () => {
    let wrapper = shallow(<App />);
    expect(wrapper.name()).toBe('div');
  });

  it('should return a name with a value of "App"', () => {
    let wrapper = mount(<App />);
    expect(wrapper.name()).toBe('App');
  });
Enter fullscreen mode Exit fullscreen mode

Again, refer to the Structure Section if your having a little problem understanding.

filter(selector: EnzymeSelector)

it returns a new wrapper based on the selector given.

  it('should have a prop of "item" with length of 3', () => {
    let wrapper = mount(<App />);
    let form = wrapper.find(Form);
    let values = ["ohio", "usa", "amawa"];
    values.forEach((value) => {
      form.invoke('handleChange')({ target: { value } });
      form.invoke('handleClick')();
    })

  expect(wrapper.find(List).find('ul li').filter('.list-item')).toHaveLength(3);

  });

});
Enter fullscreen mode Exit fullscreen mode

props()

returns the prop object of the current wrapper

  it('should have a prop "items" with a value of []', () => {
    let wrapper = shallow(<App />);
    expect(wrapper.find(List).props().items).toEqual([]);
  });


  it('should have a prop "message" with a value of "Hello World"', () => {
    let wrapper = mount(<App />);

    expect(wrapper.find(Header).props().message).toBe("Hello World");
  });

Enter fullscreen mode Exit fullscreen mode

prop(key:string)

return the value of the property of the current wrapper.

  it('should have a prop "items" with a value of []', () => {
    let wrapper = shallow(<App />);
    expect(wrapper.find(List).prop('items')).toEqual([]);
  });

  it('should have a prop "message" with a value of "Hello World"', () => {
    let wrapper = mount(<App />);

    expect(wrapper.find(Header).prop('message')).toBe("Hello World");
  });

Enter fullscreen mode Exit fullscreen mode

setProps(newProps: any)

Sets the new props object of the root node. It can only be used on the root node.


 it('should have an updated prop "message" with a value of "What the fun"', () => {
    let wrapper = mount(<App />);
    wrapper.setProps({ message: 'What the fun' })
    expect(wrapper.find(Header).prop('message')).toBe("What the fun");
  });

Enter fullscreen mode Exit fullscreen mode

setState(newState : any, callbackFunc: Function)

Sets the new state object of the root node. It can only be used on the root node.

  it('should have an updated prop "isDarkTheme" with a value of true', () => {
    let wrapper = mount(<App />);
    wrapper.setState({ isDarkTheme: true });
    expect(wrapper.state('isDarkTheme')).toBeTruthy();
  });

Enter fullscreen mode Exit fullscreen mode

simulate(event:string, ...args)

Invokes an event on the current wrapper.

  it('should have an updated value of "1234"', () => {
    let wrapper = mount(<App />);
    wrapper.find('input').simulate('change', { target: { value: '1234' } });
    expect(wrapper.state('listItem')).toBe('1234');
  });

Enter fullscreen mode Exit fullscreen mode

state(key:string)

return the value of a state property.


  it('should a input with a value of "abc"', () => {
    let wrapper = shallow(<App />);
    wrapper.setState({ listItem: 'abc' });
    expect(wrapper.state('listItem')).toBe('abc');
  });

  it('should have an updated "message" prop with a value of "Hi"', () => {
    let wrapper = mount(<App />);
    wrapper.setProps({ message: 'Hi' });
    expect(wrapper.prop('message')).toBe('Hi');
  })


Enter fullscreen mode Exit fullscreen mode

text()

returns the text of the current wrapper.

  it('should a text of "Hello World"', () => {
    let wrapper = mount(<App />);
    expect(wrapper.find('h1').text()).toBe('Hello World');
  });

Enter fullscreen mode Exit fullscreen mode

type()

returns the type of the current wrapper.


   it('should return the App', () => {
    let wrapper = shallow(<App />);
    expect(wrapper.at(0).type()).toBe('div');
  }); 

  it('should return the App', () => {
    let wrapper = mount(<App />);
    expect(wrapper.at(0).type()).toBe(App);
  });

Enter fullscreen mode Exit fullscreen mode

Check this post out to make your own Cover Image for your dev.to Post.

Thanks guys for reading this post.

Have a Nice Day 😃!.

Discussion (4)

Collapse
ramospedro profile image
Pedro • Edited on

Hey Mark!
First of all, congratulations on the great article and for sharing your knowledge. I encourage automated tests as specification, documentation and quality assurance for any software. I love writing tests and I respect A LOT people who see the value of good tests.

Seeing your article, I couldn't help but comment some of my own learnings on tests.

I've had some very painful experiences with testing that led to learnings I'll never forget.
When I started getting comfortable with writing tests, my tests looked pretty much like those you write. They seemed like very good tests and I loved seeing them all pass in my CI server. With every new push, more and more joy of seeing those tests pass.

Until I had to refactor.

Think about a colleague of yours looking at this spec in a few weeks from now: 'should have a child component of type "Header" at "index" of 0'.

Let's say this colleague has to add a new feature, which is a breadcrumb that stays above the header. As soon as he implements this, this test WILL break and it won't say anything useful.
The header is still there, in the position your users expect it to be, serving the function it should be serving. But your relentless CI Server WILL reprove this commit and your colleague is going to have to change this spec just to make it pass.

Imagine it happening every single moment in every single change in the codebase with many many tests breaking, almost like the codebase was screaming. It's chaos.

Now, if you think about it, this spec doesn't say anything about your component. If you read it to the stakeholders they won't have a clue if that's right or not. You won't be able to use this spec to communicate with anyone involved with this software, except Enzyme itself.

There are several good ways of writing frontend tests that assert and specify something meaningful and very useful for the software maintenance and lifetime.

In your case, I'd start by reading some very nice stuff from Kent Dodds and taking a look at @testing-library/react.

I hope you find my comment useful and I'm eager to see you evolve in your engineering journey :)

Collapse
macmacky profile image
Mark A Author • Edited on

Hi Pedro! I always find it hard what to write inside the 😃 it('what to write here'), do I use the method name of the method I'm gonna test? do I have to make the sentence longer so my teammates can understand or do I have to be more specific and be more technical?

But after reading your comment I finally understand that writing tests should be readable and should be easily understandable to another person specifically a teammate. I really appreciate your comment it helped me realize that writing tests and code should be the same they must be readable and should be easy so people can understand it.

I'm gonna try that library today. Thanks, man! Have a great day.

Collapse
ramospedro profile image
Pedro

Great, Mark!
I see you have great coding skills. It'll be very easy for you to get even better at writing good tests.

Collapse
giacomo9999 profile image
James Gary • Edited on

Hi -

Thanks for putting this tutorial together! One thing I noticed, however: the tests won't run (at least not on my Mac) unless the react-dom dependency is installed--

npm i -S react-dom

--and an index.js file is created at the top level of the directory (something like the image cap below):

dev-to-uploads.s3.amazonaws.com/i/...

Otherwise, great work!