score:0

Accepted answer

I found a way to do what I originally asked. I have no opinion (yet) whether it is good strategy or not (in fact I had to refactor the component immediately afterwards, so this question is no longer relevant to what I'm doing). Anyway, here is the testing code (explanation below):

import React from 'react';
import { mount } from 'enzyme';
import { MyComponent } from 'wherever';
import fetchMock from 'fetch-mock';

let _resolveHoldingPromise = false;

class WrappedMyComponent extends MyComponent {

    render() {
        const result = super.render();
        _resolveHoldingPromise && _resolveHoldingPromise();
        _resolveHoldingPromise = false;
        return result;
    }

    static waitUntilRender() {
        // Create a promise that can be manually resolved
        let _holdingPromise = new Promise(resolve =>
            _resolveHoldingPromise = resolve);

        // Return a promise that will resolve when the component renders
        return Promise.all([_holdingPromise]);
    }
}

describe('MyComponent', () => {

    fetchMock.get('*', 'some_response');

    const onError = () => { throw 'Internal test error'; };

    it('renders MyComponent appropriately', done => {
        let component = <WrappedMyComponent />;
        let wrapper = mount(component);
        WrappedMyComponent.waitUntilRender().then(
            () => {
                expect(wrapper.find('whatever')).toBe('whatever');
                done();
            },
            onError);
    });
});

The main idea is that, in the testing code, I subclass the component (if this was Python I'd probably monkey-patch it, which works more or less the same way in this case) so that its render() method sends a signal that it executed. The way to send the signal is by manually resolving a promise. When a promise is created, it creates two functions, resolve and reject, which when called terminate the promise. The way to have code outside the promise resolve the promise is by having the promise store a reference to its resolve function in an external variable.

Thanks to fetch-mock author Rhys Evans who kindly explained the manually-resolve-promise trick to me.

score:1

I'd separate concerns, mainly because is easier to maintain and to test. Instead of declaring the fetch inside the component I'd do it somewhere else, for example in a redux action (if using redux).

Then test individually the fetch and the component, after all this is unit testing.

For async tests you can use the done parameter on the test. For example:

describe('Some tests', () => {
  fetchMock.get('*', some_response);

  it('should fetch data', (done) => { // <---- Param
    fetchSomething({ some: 'Params' })
      .then(result => {
        expect(result).toBe({ whatever: 'here' });
        done(); // <--- When you are done
      });
  });
})

The you can tests your component by just sending the loaded data in the props.

describe('MyComponent', () => {

  it('renders something', () => {
    const mockResponse = { some: 'data' };
    let wrapper = mount(<MyComponent data={mockResponse}/>);

    expect(wrapper.find(...)).to.have.something();
  });
});

When it comes to testing you need to keep it simple, if your component is difficult to test, then there's something wrong with your design ;)

score:1

I've had some success with this, as it doesn't require wrapping or modifying components. It is however assuming there's only one fetch() in the component, but it can be easily modified if needed.

// testhelper.js

class testhelper
{
    static async waitUntil(fnWait) {
        return new Promise((resolve, reject) => {
            let count = 0;
            function check() {
                if (++count > 20) {
                    reject(new TypeError('Timeout waiting for fetch call to begin'));
                    return;
                }
                if (fnWait()) resolve();
                setTimeout(check, 10);
            }
            check();
        });
    }

    static async waitForFetch(fetchMock)
    {
        // Wait until at least one fetch() call has started.
        await this.waitUntil(() => fetchMock.called());

        // Wait until active fetch calls have completed.
        await fetchMock.flush();
    }
}

export default testhelper;

Then you can use it just before your assertions:

import testhelper from './testhelper.js';

it('example', async () => {
    const wrapper = mount(<MyComponent/>);

    // Wait until all fetch() calls have completed
    await testhelper.waitForFetch(fetchMock);

    expect(wrapper.html()).toMatchSnapshot();
});

Related Query

More Query from same tag