Code coverage missing even when the lines are hit

Viewed 558

I have the following code inside my useEffect. This code works fine.
Even tested the functionality via mocking and tests are fine too.

But in terms of coverage, it keeps coming up that the following 2 lines are not covered.

const check = async () => isValid(); 
check().then((result) => setValid(result));

When I try debug with breakpoints, I do hit these lines. The isValid function is being imported.
That is being tested elsewhere thus I am just mocking the outcome from the useState calls.

There is a mandatory 100% code coverage on this project I can't bypass.
Please advice how I could code cover this. Thanks.

P.S: Note that I am using following mock/spy. Without this, setCookie call loses coverage too.

jest.spyOn(React, 'useEffect').mockImplementationOnce((f) => f());

The component being tested

import React from 'react';
import Cookies from 'js-cookie';
import isValid from '../helpers/isValid';
import A from './A';
import B from './B';
import C from './C';

function AComponent() {
  return (createComponent());
}

export const createComponent = ({data}) => {
  const [valid, setValid] = React.useState(false);
  const [cookie, setCookie] = React.useState(undefined);
  React.useEffect(() => {
    if (data) {
      const check = async () => isValid(); // code coverage missing
      check().then((result) => setValid(result)); // code coverage missing
    }
    setCookie(!!Cookies.get('my-cookie')); // no issue with code coverage
  }, []);

  if (cookie) {
    return <A/>;
  } else if (valid) {
    return <B/>;
  }
  return <C/>;
};

export default AComponent;

These are the tests. The snapshot is as expected showing the 3 different outcomes.
Thus the mocking does work.

Thus looking to see how I could cover those lines even if I need to write a couple of tests
without mocking if theres a way.

jest.spyOn(React, 'useEffect').mockImplementationOnce((f) => f());
describe('Test', () => {

  const render = (props) => shallow(
    <AComponent
      {...props}
    />
  );

  it('should render A', () => {
    jest
      .spyOn(React, 'useState')
      .mockImplementationOnce(() => realUseState(false)) // mocks valid / setValid
      .mockImplementationOnce(() => realUseState(true)); // mocks cookie / setCookie
    const renderedModule = render({
        data: {a: 1}
    });
    expect(renderedModule)).toMatchSnapshot();
  });

 it('should render B', () => {
    jest
      .spyOn(React, 'useState')
      .mockImplementationOnce(() => realUseState(true)) // mocks valid / setValid
      .mockImplementationOnce(() => realUseState(false)); // mocks cookie / setCookie
    const renderedModule = render({
        data: {a: 1}
    });
    expect(renderedModule)).toMatchSnapshot();
  });

 it('should render C', () => {
    jest
      .spyOn(React, 'useState')
      .mockImplementationOnce(() => realUseState(false)) // mocks valid / setValid
      .mockImplementationOnce(() => realUseState(false)); // mocks cookie / setCookie
    const renderedModule = render({
        data: {a: 1}
    });
    expect(renderedModule)).toMatchSnapshot();
  });
}
1 Answers

It's better to test the behavior of the component instead of implementation. You'd better not mock the React useEffect, useState hooks. For example, useState is more than just a return value, it has many internal implementations, and improper mocks can change its functionality.

useEffect hook is not supported by enzyme shallow rendering. See issue#2086 and Testing a component that uses useEffect using Enzyme shallow and not mount. So we have to use mount.

E.g.

index.jsx:

import React from 'react';
import Cookies from 'js-cookie';
import isValid from './helpers/isValid';
import A from './A';
import B from './B';
import C from './C';

function AComponent(props) {
  return createComponent(props);
}

export const createComponent = ({ data }) => {
  const [valid, setValid] = React.useState(false);
  const [cookie, setCookie] = React.useState(undefined);

  React.useEffect(() => {
    if (data) {
      const check = async () => isValid();
      check().then((result) => setValid(result));
    }
    setCookie(!!Cookies.get('my-cookie'));
  }, []);

  if (cookie) {
    return <A />;
  } else if (valid) {
    return <B />;
  }
  return <C />;
};

export default AComponent;

./helper/isValid.js:

export default async () => false;

A.jsx:

import React from 'react';

export default () => <div>a</div>;

B.jsx:

import React from 'react';

export default () => <div>b</div>;

C.jsx:

import React from 'react';

export default () => <div>c</div>;

index.test.jsx:

import { mount } from 'enzyme';
import Cookies from 'js-cookie';
import React from 'react';
import A from './A';
import B from './B';
import C from './C';
import AComponent from './';
import isValid from './helpers/isValid';
import { act } from 'react-dom/test-utils';

jest.mock('./helpers/isValid');

const whenStable = async (wrapper) => {
  await act(async () => {
    await new Promise((resolve) => setTimeout(resolve, 0));
    wrapper.update();
  });
};

describe('71289768', () => {
  afterEach(() => {
    jest.restoreAllMocks();
  });
  test('should render A', () => {
    jest.spyOn(Cookies, 'get').mockReturnValue('sid:abc123');
    const wrapper = mount(<AComponent />);
    expect(wrapper.find(A).exists()).toBeTruthy();
  });
  test('should render B', async () => {
    isValid.mockResolvedValueOnce(true);
    const wrapper = mount(<AComponent data />);
    await whenStable(wrapper);
    expect(wrapper.find(B).exists()).toBeTruthy();
  });
  test('should render C', () => {
    const wrapper = mount(<AComponent />);
    expect(wrapper.find(C).exists()).toBeTruthy();
  });
});

Test result:

 PASS  stackoverflow/71289768/index.test.jsx (8.954 s)
  71289768
    ✓ should render A (39 ms)
    ✓ should render B (32 ms)
    ✓ should render C (6 ms)

------------------|---------|----------|---------|---------|-------------------
File              | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
------------------|---------|----------|---------|---------|-------------------
All files         |   94.59 |      100 |   81.82 |     100 |                   
 71289768         |     100 |      100 |     100 |     100 |                   
  A.jsx           |     100 |      100 |     100 |     100 |                   
  B.jsx           |     100 |      100 |     100 |     100 |                   
  C.jsx           |     100 |      100 |     100 |     100 |                   
  index.jsx       |     100 |      100 |     100 |     100 |                   
 71289768/helpers |   33.33 |      100 |       0 |     100 |                   
  isValid.js      |   33.33 |      100 |       0 |     100 |                   
------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        9.457 s

Note: When calling an async function inside useEffect hook and setting the state, you need to wait for the component render to complete. The whenStable function does the trick.

Related