Recoil: Testing?

Created on 20 May 2020  路  5Comments  路  Source: facebookexperimental/Recoil

What's the best method for testing functions around Recoil? For example, if I had a function like this:

export const setAddComplete =
    (addComplete: boolean, setNotesState: SetterOrUpdater<State>) => {
        setNotesState(state => {
            return {
                ...state,
                addComplete,
            };
        });
    };

How should I test it? I noticed the TestingUtils folder, but it looks like they aren't included in the package.

question

Most helpful comment

Hi @adrianbw. Recoil requires React to run. You can test your Recoil Atoms and Selectors by creating a small React component that uses them and testing that in the way you would test any React component. https://reactjs.org/docs/testing.html

For example:

const React = require("react");
const TestRenderer = require("react-test-renderer");
const Recoil = require("recoil");
const mySelector = require("../path/to/your/selector");

test("...", () => {
  let value = null;

  function TestSelector() {
    const selectorValue = Recoil.useRecoilValue(mySelector);
    React.useEffect(() => {
      value = selectorValue;
    });
    return null;
  }

  TestRenderer.act(() => {
    TestRenderer.create(<Link page="https://www.facebook.com/">Facebook</Link>);
  });

  expect(value).toEqual(expectedValue);
});

If you want to just test your state update function and not run Recoil then you could extract, export and test that code in isolation.

Before:

export const setAddComplete = (
  addComplete: boolean,
  setNotesState: SetterOrUpdater<State>
) => {
  setNotesState((state) => {
    return {
      ...state,
      addComplete,
    };
  });
};

After:

// Can directly test this function without needed Recoil
export updateState(state, addComplete) {
  return {...state, addComplete };
}

export const setAddComplete = (
  addComplete: boolean,
  setNotesState: SetterOrUpdater<State>
) => {
  setNotesState(updateState);
};

All 5 comments

I would recommend not to test like that, rather test the overall functionality by asserting on changes that should happen on the UI/screen.

I would recommend not to test like that, rather test the overall functionality by asserting on changes that should happen on the UI/screen.

I think the question is around how to unit test this function in isolation. UI Testing has its place of course. However, devs/teams may be structured in a way that UI testing is not doable by the dev writing this functionality + unit testing is good practice anyway for code that is reused in many other places (speeds up build, etc.).

Hi @adrianbw. Recoil requires React to run. You can test your Recoil Atoms and Selectors by creating a small React component that uses them and testing that in the way you would test any React component. https://reactjs.org/docs/testing.html

For example:

const React = require("react");
const TestRenderer = require("react-test-renderer");
const Recoil = require("recoil");
const mySelector = require("../path/to/your/selector");

test("...", () => {
  let value = null;

  function TestSelector() {
    const selectorValue = Recoil.useRecoilValue(mySelector);
    React.useEffect(() => {
      value = selectorValue;
    });
    return null;
  }

  TestRenderer.act(() => {
    TestRenderer.create(<Link page="https://www.facebook.com/">Facebook</Link>);
  });

  expect(value).toEqual(expectedValue);
});

If you want to just test your state update function and not run Recoil then you could extract, export and test that code in isolation.

Before:

export const setAddComplete = (
  addComplete: boolean,
  setNotesState: SetterOrUpdater<State>
) => {
  setNotesState((state) => {
    return {
      ...state,
      addComplete,
    };
  });
};

After:

// Can directly test this function without needed Recoil
export updateState(state, addComplete) {
  return {...state, addComplete };
}

export const setAddComplete = (
  addComplete: boolean,
  setNotesState: SetterOrUpdater<State>
) => {
  setNotesState(updateState);
};

For anyone who comes upon this and likes to write unit tests (a discussion I'm not going to get into), here's a pattern I adopted, which uses jest.fn() to report out the atom's value.

interface IGenericUpdater<ValType, StateType extends object> {
    (value: ValType, setState: SetterOrUpdater<StateType>): void;
}

interface ISetterTest<U, V extends object> {
    atom: Atom;
    function: IGenericUpdater<U, V>;
    value: U;
}

type UpdaterFunction = (property: any, updaterFunction: SetterOrUpdater<any>) => void;

type TestComponentProps = {
    atom: Atom;
    function: UpdaterFunction;
    value: any;
    reporterFunction: (state: State) => void;
};

const TestComponent: React.FunctionComponent<TestComponentProps> = (props: TestComponentProps) => {
    const [state, setState]: [State, any] = useRecoilState(props.atom);
    React.useEffect(() => {
        props.function(props.value, setState);
        props.reporterFunction(state);
    },              [state]);
    return <div />;
};

const createMountedWrapper = (additionalProps: Partial<TestComponentProps>) => {
    const props = {
        atom: null as any,
        function: jest.fn() as any,
        value: null as any,
        reporterFunction: jest.fn() as any,
        ...additionalProps,
    };
    const wrapper = mount(<RecoilRoot><TestComponent {...props} /></RecoilRoot>).rendered;
    const instance = wrapper.find(TestComponent).instance() as any;
    return {wrapper, instance, ...props};
};

describe('recoil tests', () => {
    let componentWrapper: any;
    afterEach(() => {
        if (componentWrapper?.unmount) {
            componentWrapper.unmount();
        }
    });
   it('setAddComplete', () => {
        const props: ISetterTest<boolean, State> = {
            atom: notesStore,
            function: setAddComplete,
            value: true,
        };
        const { wrapper, reporterFunction } = createMountedWrapper(props);
        componentWrapper = wrapper;
        expect(reporterFunction).toHaveBeenCalledWith({...initState, addComplete: props.value});
    });
});

Before adding wrapper.unmount(), I got in some loops with useEffect testing multiple functions (presumably because it was adding an instance on every mount?). There may be other ways to prevent this that I don't know of.

You can now use snapshots for testing outside of React.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jamiewinder picture jamiewinder  路  3Comments

ymolists picture ymolists  路  3Comments

atanasster picture atanasster  路  3Comments

polemius picture polemius  路  3Comments

thegauravthakur picture thegauravthakur  路  3Comments