Q:

Spy on a function inside a component method

For example I have
import React from 'react';
import { getProducts } from 'api/getProducts';

export class Example extends React.Component {
    state = {
        items: []
    };

    componentDidMount() {
        this.fetchProducts();
    }

    fetchProducts = async () => {
        const { products: items } = this.state;
        if (items.length > 0) {
            return;  //TEST COVERAGE HERE MISSING
        }
        try {
            const { result } = await getProducts('/api/product/1');
            this.setState({ items: result });
        } catch (e) {
            // no-op
        }
    };

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

My test case is missing a coverage where it returns early if items already exists.
import React from 'react';
import { getProducts } from 'api/getProducts';
import { Example } from './Example';

jest.mock('api/getProducts');

describe('Example', () => {
    it('do not call getProducts if it already exists in state', async () => {
        const wrapper = mount(<Example />);
        const instance = wrapper.instance();
        instance.setState({
            items: [{ id: 1, name: 'kettle' }]
        });

        await instance.fetchProducts();

        //How to spy on getProducts and assert it hasn't been called?

    });
});

A:

You can use jest.mock(moduleName, factory, options) method to mock api/getProducts module.
E.g.
index.tsx:
import React from 'react';
import { getProducts } from './api/getProducts';

export class Example extends React.Component {
  state: any = {
    items: [],
  };

  componentDidMount() {
    this.fetchProducts();
  }

  fetchProducts = async () => {
    // @ts-ignore
    const { items } = this.state;
    if (items.length > 0) {
      return;
    }
    try {
      const { result } = await getProducts('/api/product/1');
      this.setState({ items: result });
    } catch (e) {
      // no-op
    }
  };

  render() {
    const { items } = this.state;
    return (
      <div>
        <ul>
          {items.map((item) => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ul>
      </div>
    );
  }
}

api/getProducts.ts:
export async function getProducts(url) {
  return { result: [] };
}

index.spec.tsx:
import React from 'react';
import { Example } from './';
import { mount } from 'enzyme';
import { getProducts } from './api/getProducts';

jest.mock('./api/getProducts.ts', () => {
  return {
    getProducts: jest.fn(),
  };
});

describe('Example', () => {
  afterEach(() => {
    jest.resetAllMocks();
  });
  it('should fetch products correctly', async () => {
    const wrapper = mount(<Example></Example>);
    const instance = wrapper.instance();
    wrapper.setState({
      items: [{ id: 1, name: 'kettle' }],
    });
    await instance['fetchProducts']();
    expect(getProducts).toBeCalledWith('/api/product/1');
  });
});

Unit test result with 100% coverage: 
 PASS  src/stackoverflow/59241777/index.spec.tsx
  Example
    ✓ should fetch products correctly (56ms)

-----------|----------|----------|----------|----------|-------------------|
File       |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files  |      100 |      100 |      100 |      100 |                   |
 index.tsx |      100 |      100 |      100 |      100 |                   |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        5.273s, estimated 10s

