Skip to content

star2star/testing-help

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 

Repository files navigation

testing-help

Introduction

This document contains some solutions for common issues that we have come across while unit testing our React components. Our test suite utilizes Jest CLI, Enzyme, and SinonJS.

Table of Contents

Testing Props inside a class method

When components become more complex, we rely on class methods to help accomplish multiple things on a single event handler. However, when a prop is invoked inside of a class method, it's invocation is masked to us during testing. For a little bit of background: Select is a component that has a callback function prop that is called cbOnClick. The cbOnClick prop is invoked within the class method toggleSelect on mousedown. Below is a condensed version of the Select class and render methods.

Example:

	// IN THE SELECT CLASS

	toggleSelect(){
		this.setState({ ...this.state, isOpen: !this.state.isOpen, hasError: false });
		this.props.cbOnClick(); // toggleSelect calls cbOnClick
	}

	// IN THE SELECT RENDER METHOD

	<div
		 className="dropDownMenu"
		 onMouseDown={this.toggleSelect} // toggle select is called onMouseDown. NOT this.props.cbOnClick
	>
		 <div className="defaultSelectedItem">
				{this.props.data[this.state.selectedItem]}
		 </div>
		 <span style={this.svgContainerStyle} >
				<CaretDownIconSVG svgStyle={svgStyles}/>
		 </span>
	</div>

Normally, we would use SinonJS to call an anonymous spy on mousedown. The example below is how we would normally test props that are callback functions.

Example:

	it('toggleSelect fired', () => {
		const spy = sinon.spy(); // an anonymous spy is defined

		const wrapper = shallow(
			<Select
					customLabel="Favorite Hockey Team"
					data={hockeyTeams}
					cbOnClick={spy} // anonymous spy is assigned to cbOnClick
			/>);

		wrapper.find('.dropDownMenu').simulate('mousedown'); // mousedown event is simulated.
		expect(spy.calledOnce).toEqual(true); // testing that cbOnClick was called (false)

	});

But, the above test returns false. In this test, spy will never be called because the onMouseDown event does not call cbOnClick-- it calls the toggleSelect method. By simply adding the object and method arguments to sinon.spy(), we can spy on a specific method in our component. In our case, object would be Select.prototype and method would be 'toggledSelect'.

Example:

	it('toggleSelect fired', () => {
		const spy = sinon.spy(Select.prototype, 'toggleSelect');// Here we are adding the object and method arguments.

		const wrapper = shallow(
			<Select
					customLabel="Favorite Hockey Team"
					data={hockeyTeams}
					cbOnClick={spy}
			/>);

		wrapper.find('.dropDownMenu').simulate('mousedown');
		expect(spy.calledOnce).toEqual(true); // now true
	});

However, now we are only testing that toggleSelect is being called. We still must know if cbOnClick is called. Since toggleSelect is now being invoked, we can verify that cbOnClick is being called as well by simply adding an anonymous spy to the cbOnClick prop.

Example:

	it('toggleSelect fired', () => {
		const spy = sinon.spy(Select.prototype, 'toggleSelect');
		const spy2 = sinon.spy(); // an anonymous spy

		const wrapper = shallow(
			<Select
					customLabel="Favorite Hockey Team"
					data={hockeyTeams}
					cbOnClick={spy2} // assigning anonymous spy to cbOnClick
			/>);

		wrapper.find('.dropDownMenu').simulate('mousedown');
		expect(spy.calledOnce).toEqual(true); // testing that toggleSelect is called.
		expect(spy2.calledOnce).toEqual(true); // testing that spy2 is called.
	});

Adding these arguments to spy also allows us to be more in depth with your tests. For instance, since toggleSelect also sets state when called, a test can be made to verify that state is correctly set.

Example:

	it('toggleSelect fired', () => {
		const spy = sinon.spy(Select.prototype, 'toggleSelect');
		const spy2 = sinon.spy(); // an anonymous spy

		const wrapper = shallow(
			<Select
					customLabel="Favorite Hockey Team"
					data={hockeyTeams}
					cbOnClick={spy2} // assigning anonymous spy to cbOnClick
			/>);

		wrapper.find('.dropDownMenu').simulate('mousedown');
		expect(spy.calledOnce).toEqual(true);
		expect(spy2.calledOnce).toEqual(true);
		expect(wrapper.state().isOpen).toEqual(true); // testing that state is setting correctly
 });
 

Testing User Input

I ran into a situation where I had to test a callback function of Reset component in Login test. In order for this click event to fire, the two input values must be valid and matching. Normally, we would use setProps or setState but those solutions were not available to me due to the fact that:

  • Reset does not have a prop value that I can set for input values.
  • Reset is a child component in my login test, so I can not effectively set state for Reset's inputs.

The initial thought was to set the value of the inputs in the Reset component in the Login test by simulating multiple keypresses that would spell out an email. But, it was a repetitive and not-so-nice solution.

DO NOT:

const input1 = wrapper.childAt(2).find('.loginBlock').childAt(1).find('input'); // finding my input

input1.simulate('change', { keyCode: 75 }).('change', { keyCode: 67 }).('change', { keyCode: 50 }).('change', { keyCode: 71 }).('change', { keyCode: 77 }).('change', { keyCode: 65 }).('change', { keyCode: 73 }).('change', { keyCode: 76 }).('change', { keyCode: 190 }).('change', { keyCode: 67 }).('change', { keyCode: 79 }).('change', { keyCode: 77 }); // simulating keypresses that should spell out an email

Fortunately, there is a much simpler solution.

DO:

const input1 = wrapper.childAt(2).find('.loginBlock').childAt(1).find('input');

input1.simulate('change', { target: { value: '[email protected]'}});

Testing componentWillReceiveProps

To ensure that the state updates properly with a new change when ther props acts as the default.

Testing shouldComponentUpdate

  it('rerenders when props have changed', () => {
    const spy = sinon.spy(Button.prototype, "render"); // if shouldComponentUpdate ends up being true, the render method will be fired.

    const wrapper = shallow(<Button cbClick={funFunc} buttonLabel="John Adams spends the summer with his family" svgType="SettingsIconSVG" svgPos="bottom" />);

    // render is always called once so before setting any props check that it was called once.
    expect(spy.calledOnce).toEqual(true);

    // After setting props check to see if render has been called twice (initial render and render after props have changed)
    wrapper.setProps({ buttonLabel: "Work"});
    expect(spy.calledTwice).toEqual(true);

    // Now checking to see if render is called again even though props are the same. Should be false.
    wrapper.setProps({ buttonLabel: "Work"});
    expect(spy.calledThrice).toEqual(false);
  });

Testing functions with setTimeout

  • Coming soon.

Common simulate arguments

Enzyme provides a simulate() function that takes in two arguments. The first is what DOM event to simulate and the second is an optional parameters. We have compiled a list of commonly used simulate() arguments

Testing a click

sinon.simulate('click')

Testing onFocus

sinon.simulate('focus')

Testing onBlur

sinon.simulate('blur')

Testing onChange

sinon.simulate('change', { target: {value: 'password1'})

Testing onMouseDown

sinon.simulate('mousedown')

Testing onMouseUp

sinon.simulate('mouseup')

Testing onMouseEnter

sinon.simulate('mouseenter')

Testing onMouseLeave

sinon.simulate('mouseleave')

Testing onMouseOver

sinon.simulate('mouseover')

Testing onKeyPress

sinon.simulate('keypress')

sinon.simulate('keypress', { keycode: 32 }); // enter

Testing onKeyDown

sinon.simulate('keydown')

sinon.simulate('keydown', { keycode: 32 }); // enter

Testing onKeyUp

sinon.simulate('keyup')

sinon.simulate('keyup', { keycode: 13 }); // spacebar

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published