-
Notifications
You must be signed in to change notification settings - Fork 25
Unit tests
We use Testing Library for UI unit tests.
It gets a component and renders it in a sandbox environment (click here to see why this approach). Then we can assert the rendered DOM using snapshots or query helpers.
There's a catch though. As we know components can use hooks, some of the hooks are asynchronous. This means the component will render several times before getting to a stable state. In the browser React updates the page with every re-render cycle. In a test, we need to tell explicitly "hey, wait for another cycle and then continue". The way to do that is to call the act
function (if interested, you could start from FAQ "How do I fix 'an update was not wrapped in act(...)' warnings?"), use async utilities, or await for our waitForEffect helper.
To view console output, you must run the test(s) in no-silent mode (npm run test:watch -- --no-silent
). By default our tests are configured to only output test failures.
Since testing of components relies on the rendered output of the component you may want to see the exact HTML produced during a test. Use screen.debug()
for that:
import { render, screen } from "@testing-library/react";
render(<Component />);
const someInput = screen.getByLabelText("Field label");
screen.debug(); // to see the whole rendered output
screen.debug(someInput); // to see the HTML of the someInput element
- try to have one
expect
per test; - use
test.each
for multiple test cases of the same functionality (you can have the test name, value to test and expected value in the array); - use contextual matchers instead of generics, favor
.toBeNull
over.toBe(null)
, use HTML element matches like.toHaveClass
, use.not
to negate the matcher like.not.toBeNull
.
Snapshot testing means that the test will render a component save the rendered HTML in a snapshot file and on the next run it will compare the freshly rendered HTML with the snapshot. Snapshot tests are simple to create but may be cumbersome to maintain. Keep in mind that when you add a snapshot test for a page, a change in any component used on that page will fail the test. Snapshot tests are good as unit tests for small components (like SwitchButtonWidget) or as integration tests for bigger components involving several moving parts.
See examples:
The test still renders the component but instead of comparing the markup to the saved snapshot, the test expects to find (or not to find) certain elements or text content in the rendered DOM. To access the rendered DOM you can use Queries (preferable) or element query selectors. Trigger user events like clicks or text input as needed (FormEditor has a lot of examples).
See examples:
Just regular unit testing.
See examples:
Approach #1: use waitForEffect
Approach #2: split the component into a presentational (sometimes call "dumb") component and a stateful component (sometimes called a "smart" or "connected" component)
Approach #3: split the useAsyncState value generators/factories out into helper methods. Use jest mocks to mock the return value of the factory (using mockResolvedValue
for the resolved promise, or a regular mock for an unresolved promise). If a component has multiple useAsyncState uses, you may want to combine them into a single useAsyncState call which returns a single promise (just be sure to use Promise.all where appropriate to enable network request promises to run concurrently)
const state = useAsyncState(async () => helper(foo, bar), [foo, bar]);
If you need to create predictable timestamps, you can use the mockdate library:
import MockDate from "mockdate";
const timestamp = new Date("10/31/2021");
MockDate.set(timestamp);
If you need to test a component that requires Router context, you can wrap the component under test with the StaticRouter.
import { StaticRouter } from "react-router-dom";
...
<StaticRouter>
<InstalledPage extensions={[]} push={jest.fn()} onRemove={jest.fn()} />
</StaticRouter>
Example: InstalledPage.test
Add <Formik>
around your component. You can use createFormikTemplate
helper from formHelpers
const FormikTemplate = createFormikTemplate({
[RJSF_SCHEMA_PROPERTY_NAME]: {
schema,
uiSchema: {},
} as RJSFSchema,
});
render(
<FormikTemplate>
<FormEditor activeField={fieldName} {...defaultProps} />
</FormikTemplate>
);
Example: FormEditor.test
If you need to mock a module with different implementations per test, you can use mockImplementation
after mocking & importing the mocked module. Clean up the mock after a test run with mockClear() or mockReset()
jest.mock("packageToMock", () => ({
methodToMock: jest.fn()
}));
import { methodToMock } from "packageToMock";
afterEach(() => {
// clear the mock usage
methodToMock.mockClear();
// or make a deep cleaning including implementations and return values
// methodToMock.mockReset();
});
test("test foo", () => {
methodToMock.mockImplementation(() => "foo");
// or
// methodToMock.mockImplementationOnce(() => "foo");
// or
// methodToMock.mockReturnValue("foo");
// or
// methodToMock.mockReturnValueOnce("foo");
// see docs at https://jestjs.io/docs/mock-function-api
});
test("test bar", () => {
methodToMock.mockImplementation(() => "bar")
});
Example: InstalledPage.test
Alternatively, you can look into doMock
.
Important: __esModule: true
is required for the default export of the module to work. Otherwise imports of the default module will resolve to a plain object with a "default" property
Example: LocalProcessOptions.test.tsx
jest.mock("@/components/form/widgets/RemoteSelectWidget", () => {
// The jest.requireActual must be its own statement. You cannot spread it in the returned object
const mock = jest.requireActual(
"@/components/form/widgets/RemoteSelectWidget"
);
return {
// Required for the default of the export module to work
__esModule: true,
...mock,
// The things you actually want to mock
useOptionsResolver: jest.fn().mockReturnValue([[], false, undefined]),
};
});
Example: LocalProcessOptions.test.tsx
Return the mock directly from the mock factory function:
jest.mock("@/services/useDependency", () =>
jest.fn().mockReturnValue({
// Omitted for brevity
})
);