How to test a prop update on React component
Asked Answered
H

9

54

What is the correct way of unit testing a React component prop update.

Here is my test fixture;

describe('updating the value', function(){
        var component;
        beforeEach(function(){
            component = TestUtils.renderIntoDocument(<MyComponent value={true} />);
        });

        it('should update the state of the component when the value prop is changed', function(){
            // Act
            component.props.value = false;
            component.forceUpdate();
            // Assert
            expect(component.state.value).toBe(false);
        });
});

This works fine and the test passes, however this displays a react warning message

'Warning: Dont set .props.value of the React component <exports />. Instead specify the correct value when initially creating the element or use React.cloneElement to make a new element with updated props.'

All i want to test is the update of a property, not to create a new instance of the element with a different property. Is there a better way to do this property update?

Hoskins answered 3/6, 2015 at 8:15 Comment(0)
T
69

AirBnB's Enzyme library provides an elegant solution to this question.

it provides a setProps method, that can be called on either a shallow or jsdom wrapper.

    it("Component should call componentWillReceiveProps on update", () => {
        const spy = sinon.spy(Component.prototype, "componentWillReceiveProps");
        const wrapper = shallow(<Component {...props} />);

        expect(spy.calledOnce).to.equal(false);
        wrapper.setProps({ prop: 2 });
        expect(spy.calledOnce).to.equal(true);
    });
Tabber answered 18/2, 2016 at 22:0 Comment(3)
But the OP didn't mentioned Enzyme, neither in their tagsIous
What if we have mounted the component ?Lemuellemuela
I would not recommend testing using this approach in '21 although I don't know specifically what you are testing I would be focused on asserting that the application of the prop change is correctly rendered in the component output rather than the value of the prop itself.Tabber
A
53

If you re-render the element with different props in the same container node, it will be updated instead of re-mounted. See React.render.

In your case, you should use ReactDOM.render directly instead of TestUtils.renderIntoDocument. The later creates a new container node every time it is called, and thus a new component too.

var node, component;
beforeEach(function(){
    node = document.createElement('div');
    component = ReactDOM.render(<MyComponent value={true} />, node);
});

it('should update the state of the component when the value prop is changed', function(){
    // `component` will be updated instead of remounted
    ReactDOM.render(<MyComponent value={false} />, node);
    // Assert that `component` has updated its state in response to a prop change
    expect(component.state.value).toBe(false);
});
Auxochrome answered 3/6, 2015 at 9:29 Comment(5)
In actual practice, the render is not called explicitly in all cases. A better way is to use state data change to trigger the re-render as there may be some other hooks in live environment which you are missing out on by directly calling state change.Tomboy
If you're testing a response to props changes in something like componentWillReceiveProps or componentDidUpdate, etc., this is how I would do it as well. That said, I would try to rewrite the component so that the value in state is calculated dynamically at render-time instead, if possible (instead of using state).Television
+1 for the answer. Note that this is now ReactDOM.render instead of just React.render (as of version 0.14 I think).Intimate
Both TestUtils.renderIntoDocument and ReactDOM.render uses the returned value from ReactDOM.render. According to React docs: "using this return value is legacy and should be avoided because future versions of React may render components asynchronously in some cases". Is there a better solution which avoids the use of ReactDOM.render output?Decile
what if MyComponent is wrapped around a Provider ?Germanophile
M
17

Caveat: this won't actually change props.

But for me, all I wanted was to test my logic in componentWillReceiveProps. So I'm calling myComponent.componentWillReceiveProps(/*new props*/) directly.

I didn't need/want to test that React calls the method when props change, or that React sets props when props change, just that some animation is triggered if the props differ to what was passed in.

Moya answered 20/10, 2015 at 22:21 Comment(0)
B
11

Quick addition, as I was looking for an answer for testing-library, and didn't find it here: there's an example in this issue, and it looks like this:

const {container} = render(<Foo bar={true} />)

// update the props, re-render to the same container
render(<Foo bar={false} />, {container})

Alternatively, testing-library now offers a rerender method that accomplishes the same thing.

Baguette answered 16/1, 2020 at 17:24 Comment(0)
E
1

This is an older question, but in case anyone else stumbles upon this, the following setup has worked for me nicely:

it('updates component on property update', () => {
    let TestParent = React.createClass({
        getInitialState() {
            return {value: true};
        },
        render() {
            return <MyComponent value={this.state.value}/>;
        }
    });
    component = TestUtils.renderIntoDocument(<TestParent/>);
    component.setState({value: false});
    // Verification code follows
});

This makes React run the usual component update.

Ebullience answered 15/11, 2016 at 12:45 Comment(0)
F
1

Here's a solution I've been using that uses ReactDOM.render but doesn't rely on the (deprecated) return value from the function. It uses the callback (3rd argument to ReactDOM.render) instead.

Setup jsdom if not testing in the browser:

var jsdom = require('jsdom').jsdom;
var document = jsdom('<!doctype html><html><body><div id="test-div"></div></body></html>');
global.document = document;
global.window = doc.defaultView;

Test using react-dom render with async callback:

var node, component;
beforeEach(function(done){
    node = document.getElementById('test-div')
    ReactDOM.render(<MyComponent value={true} />, node, function() {
        component = this;
        done();
    });
});

it('should update the state of the component when the value prop is changed', function(done){
    // `component` will be updated instead of remounted
    ReactDOM.render(<MyComponent value={false} />, node, function() {
        component = this;
        // Assert that `component` has updated its state in response to a prop change
        expect(component.state.value).toBe(false);
        done();
    });
});
Furriery answered 13/2, 2017 at 16:37 Comment(0)
G
1

You can use enzyme to mount components and add props to it:

import React form 'react';
import component;
import {configure, mount} form 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import {expect} from 'chai';

configure({adapter: new Adapter()});

describe('Testing component', () => {
  let wrapper;
  beforeEach(() => {
    component = mount(<MyComponent value={false} />);
  });
  it('should update the state of the component when the value prop is changed', function(){

    expect(component.props().children.props.value).toBe(false);
});
Gynandromorph answered 3/8, 2019 at 7:0 Comment(0)
D
0

Both TestUtils.renderIntoDocument and ReactDOM.render uses the returned value from ReactDOM.render. According to React docs:

ReactDOM.render() currently returns a reference to the root ReactComponent instance. However, using this return value is legacy and should be avoided because future versions of React may render components asynchronously in some cases. If you need a reference to the root ReactComponent instance, the preferred solution is to attach a callback ref to the root element

What if we take this advise and do something like this:

let component, node;

const renderComponent = (props = {}) => {
  ReactDOM.render(<MyComponent ref={r => component = r} {...props} />, node);
}

beforeEach(function(){
    node = document.createElement('div');
    renderComponent({value: true}, node); 
});

it('should update the state of the component when the value prop is changed', function(){
    // `component` will be updated instead of remounted
    renderComponent({value: false}, node); 
    // Assert that `component` has updated its state in response to a prop change
    expect(component.state.value).toBe(false);
});
Decile answered 20/12, 2016 at 10:10 Comment(0)
F
0

The last method (with ReactDOM.render(<MyComponent ref=...) does not work with HOC components.

Fictile answered 16/8, 2021 at 9:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.