DEV Community

ZhiHong Chua
ZhiHong Chua

Posted on • Edited on

From adopting spaghetti code to 100% test coverage

// JEST and Enzyme for React Native App

Also see part 2 here: Why Shallow Tests?

Well, hopefully you reading this are not given a short few days to write tests for something you never seen before. That was my experience, but it happened because I was trying to help my team hit the crazy deadline that was just over
Image description

Disclaimer

  • Unless your APP / program is REALLY huge (like Facebook), manual testing may be more time-savvy than writing unit tests
  • This article summarizes all my discoveries over 3 work days writing tests. Hopefully that gets your tests up faster

Benefits of testing

  1. Catch bugs you did not see before (eg. 2 exported functions with same name but from different files, and slightly different implementation).
  2. Understand code that you haven't seen before.

Table of Contents

  1. File Naming
  2. Test DOM props OR function returns?
  3. Mocking Functions
  4. Mocking Hooks
  5. async / awaits
  6. Debug failing tests
  7. Others

1. File Naming

For ease of reference, just add .test. to your actual file(s) name. Mimicking the folder structure will help organize things too. In this case, we have a component ITerms within the page IdemnityForSpaceTravel.

Image description Image description

2. Test DOM props OR function returns

This article will focus on mount() method from Enzyme. If you are just mocking function return values, don't waste time here! Jest documentation should be plenty to get started.

describe('@pages/IdemnityForSpaceTravel/ITerms', () => {
  it('getIdemnityIterms() is called correctly', () => {
    // const wrapper =
    mount(
      <Wrap>
        <ITerms />
      </Wrap>
    );
    expect(getIdemnityIterms).toBeCalled();
  });
...
});
Enter fullscreen mode Exit fullscreen mode

3. Mocking (nested) Functions

  • mocking default export function
jest.mock('./esModule', () => ({
  __esModule: true, // this property makes it work
  default: 'mockedDefaultExport',
}));
Enter fullscreen mode Exit fullscreen mode
  • mocking named export function(s)
jest.mock('@pages/IdemnityForSpaceTravel/ITerms/config', () => ({
  getIdemnityIterms: jest.fn(),
  getUserType: jest.fn(),
});
Enter fullscreen mode Exit fullscreen mode
  • partial mocking named export function(s) Uses some actual functions from the file.
jest.mock('@pages/IdemnityForSpaceTravel/ITerms/config', () => ({
  ...jest.requireActual('@pages/IdemnityForSpaceTravel/ITerms/config'),
  getIdemnityIterms: jest.fn(),
});
Enter fullscreen mode Exit fullscreen mode
  • mocking a nested function (with Promise / without Promise)
    This is for the case when you want to mock a function within the function on your page / component. Use the same methods as above, but find the right file to import from.

  • mocking actions
    Button presses:
    wrapper.find(TouchableWithoutFeedback).prop('onPress')?.(); or wrapper.find(TouchableWithoutFeedback).simulate('click')

4. Mocking Hooks

Sometimes, Jest isnt that helpful in telling you why it fails...
Image description

  • Custom Hooks

Let's say you had a custom hook useError.tsx that has the structure of

export default () => {
  ...
  return { ErrorComponent, getErrorComponent };
};
Enter fullscreen mode Exit fullscreen mode

used in main file like const { ErrorComponent } = useError();, do this:

jest.mock('@utils/index', () => {
  return {
    __esModule: true,
    useError: () => ({ ErrorComponent: 'mocked_value' })
  };
});
Enter fullscreen mode Exit fullscreen mode
  • useState() or useEffect() hooks
    In the case where you have useEffect(() => { ... }, []) that runs on initial render, updating the state of the page / component, use wrapper.update() to update the DOM props.

  • redux useSelector() hooks
    In this case, this is what you need in the .test. file.

import * as redux from 'react-redux';
...
const mockUsername = 'zelenskyyy';
...
describe('@pages/IdemnityForSpaceTravel', () => {
  beforeEach(() => {
    const spy = jest.spyOn(redux, 'useSelector');
    spy.mockReturnValue({
      username: mockUsername
    });
  });
  ...
});
Enter fullscreen mode Exit fullscreen mode

5. async / awaits

Flushing Promises are a good way to ensure the async calls are completed before you assert tests. In below example, we see how we mock test an async function getIdemnityIterms in (1) Promise Resolved, (2) Promise Rejected.

// definition for flushing promises
const flushPromises = () => new Promise(setImmediate)

// mock the function as a generic jest.fn
jest.mock('@pages/IdemnityForSpaceTravel/ITerms/config', () => {
  return {
    __esModule: true,
    getIdemnityIterms: jest.fn(),
  };
});

// tests
describe('@pages/IdemnityForSpaceTravel', () => {
  it('promise works out', async () => {
    // mock jest.fn implementation: Promise is resolved
    (getIdemnityIterms as any).mockImplementation(() =>
      Promise.resolve({ ITerms: 'this is my iterm' });

    const wrapper = mount(
      <Wrap>
        <ITerms>
      </Wrap>
    );

    await flushPromises();
    wrapper.update();

    expect(getIdemnityIterms).toBeCalled();
    expect(wrapper.find(Text).prop('value')).toBe('this is my iterm');
    // Finds the 'value' of Text component in ITerms
  });

  it('feeling betrayed', async () => {
    // mock jest.fn implementation: Promise is rejected
    (getIdemnityIterms as any).mockImplementation(() =>
      Promise.reject());

    const wrapper = mount(
      <Wrap>
        <ITerms>
      </Wrap>
    );

    await flushPromises();
    wrapper.update();

    expect(getIdemnityIterms).toBeCalled();
    expect(wrapper.find(Text).prop('value')).toBe('');
    // Finds the 'value' of Text component in ITerms
  });
...
});
Enter fullscreen mode Exit fullscreen mode

6. Debug failing tests

1. Use Jest built-in Istanbul to see which lines need coverage.
Run Jest with --coverage flag, open the coverage/Icov-report/index.html in Finder / File Explorer and double-click it. This should give you all the files and how much coverage it has.
Image description Open each file to see which lines are not covered.
Image description Here, RED lines 44 and 45 means it is not covered.

2. Comment out everything
Comment out everything. Then uncomment each component in the render tree and run test. This helps you figure out where the test is failing. You can even do this at the prop level

3. Log the DOM Tree
console.log(wrapper.debug()) to see the whole render tree.
Image description // Would you just look at the <View> ?
If you have a very deep-nested DOM tree like above, you can zoom into each part like

console.log(wrapper.find(Touchable).debug()) // find all Touchable component(s)
console.log(wrapper.find(Touchable).at(1).debug()) // find Touchable component at position 1 (0-indexed)
console.log(wrapper.find(Touchable).find(TextInput).debug()) // find the TextInput component inside the Touchable component
Enter fullscreen mode Exit fullscreen mode

4. Take a break!
Sometimes you just forgot to save changes, or typo-ed somewhere.

7. Others

1. Jest configurations
Having looked at so much for tests in such short time, I just feel like
Image description Go figure this yourself!

2. Running Individual tests
This is mainly to reduce time spent on debugging tests.

Option 1: Running test with flag
With this, you can get to run individual test files. For example, if you have a test file Spaghetti.test.tsx, run npm run test -- Spaghetti.test.tsx

Option 2: Adding a VSCode Plugin
Image description Check this post on how to start it.
Pro: Quicker test debugging. Runs single tests instead of ALL existing tests.
Con: Initial setup time required.

3. Test Coverage
Run JEST --coverage flag to see how much tests are covering. Things like if / else branch, function call coverage.
Image description

4. React Testing Library
Some forums say it is better, maybe it is a viable alternative.

Top comments (0)