Lennert Hofman
Lennert Hofman

Reputation: 595

How to wait in enzyme for a promise from a private function?

I'm a novice at react and any javascript testing frameworks.

I have a simple component that retrieves an item from the API and shows them to the screen.

The function getItems() is called from componentWillMount.

Is it possible to wait until getItems() has finished before making my assertions?

ItemDetails.js

class ItemDetails extends Component {
    constructor(props) {
        super(props);
        this.state = {
            details: ''
        }
    }

    componentWillMount() {
        this.getItem();
    }

    getItem() {
        const itemId = this.props.match.params.id;
        fetch(`/api/items/${itemId}`)
            .then(res => res.json())
            .then(details => this.setState({ details }));
    }

    render() {
        const details = this.state.details;
        return (
            <div>
                <h1>{details.title}</h1>
                ...
            </div>
        );
    }
}

export default ItemDetails;

ItemDetails.test.js

describe('ItemDetails', () => {
    it('should render a div with title', () => {
        const details = {
            _id: 1,
            title: 'ItemName'
        };
        fetch.mockResponseOnce(JSON.stringify(details));
        const wrapper = mount(<ItemDetails match={{ params: {id: 1} }} />);
        expect(wrapper.find('div').find('h1').text()).toBe('ItemName');

    });
});

Upvotes: 7

Views: 9422

Answers (2)

qnilab
qnilab

Reputation: 474

The answer above works, but it invites to test implementation details:

  • as a rule of thumb, accessing the instance of a wrapper (.instance()) and calling update() are smells of tests that know too much about the way the code under test works in my opinion, it is not testing a behavior, it is testing an implementation
  • having the method return a promise and wait on that promise before executing the expectation breaks the privacy of the method: it make the code hard to refactor

What you really want is to wait on all promises to be resolved, without accessing a handle to those promises (that would break privacy). Try this

const wait = () => new Promise(resolve => setTimeout(resolve));

it('does something', () => {
  renderFnThatCallsAPromiseInternally();

  return wait().then(() => {
    expect(somethingDependentOnPromiseExecution).to.be.present();
  });
});

This will wait for all inner promises to resolve, provided wait is called after the code that enqueues promises (rendering the component) is called. The reason lies in how the JS event loop works, it's finicky, but very well explained by Jake Archibald in this talk: https://www.youtube.com/watch?v=cCOL7MC4Pl0.

Hope that helps.

Upvotes: 9

Tomasz Mularczyk
Tomasz Mularczyk

Reputation: 36179

Could you try:

describe('ItemDetails', () => {
    it('should render a div with title', () => {
        const details = {
            _id: 1,
            title: 'ItemName'
        };
        fetch.mockResponseOnce(JSON.stringify(details));
        const wrapper = shallow(<ItemDetails match={{ params: {id: 1} }} />);      

        // manually call function
        wrapper.instance().getItem();
        // update to re-render component
        wrapper.update();

        expect(wrapper.find('div').find('h1').text()).toBe('ItemName');    
    });
});

If it doesn't help I think you will need to return Promise from your function (base on this example):

getItem() {
    const itemId = this.props.match.params.id;
    return fetch(`/api/items/${itemId}`)
        .then(res => res.json())
        .then(details => this.setState({ details }));
}


describe('ItemDetails', () => {
    it('should render a div with title', () => {
        const details = {
            _id: 1,
            title: 'ItemName'
        };
        fetch.mockResponse(JSON.stringify(details)); //response gets called twice
        const wrapper = mount(<ItemDetails match={{ params: {id: 1} }} />);

        // return Promise so Jest will wait until it's finished
        return wrapper.instance().getItem().then(() => {
          wrapper.update();
        }).then(() => {
         expect(wrapper.find('div').find('h1').text()).toBe('ItemName'); 
        })
    });
});

Upvotes: 3

Related Questions