-
Notifications
You must be signed in to change notification settings - Fork 22
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.
- 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
.
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.
NOTE: you must do this for each test run configuration. If you rerun a single test from a run, you'll have to follow these steps again fo the configuration IntelliJ created for running that single test
- Open the Run/Debug Configuration for the test
- Find the "Jest options" entry for the test
- Add --no-silent
Pro-tip: update the Jest configuration template to include --no-silent. (Does not affect existing configurations)
- Click "Edit configuration templates"
- Click "Jest"
- Add --no-silent to the "Jest options" entry
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
- Unit testing a plain function utils.test
- More complex unit tests partnerIntegrations.test
- A React component tests ArrayWidget.test
- More complex functional testing of a React component (with mocks, user events, handling async actions, etc) EditorPane.test
- Testing a transformer brick parseJson.test.ts
- Testing a brick with subpipelines IfElse.test.ts
- Tests referencing Redux state from the background page and using the browser API deployments.test.ts
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 (EditorPane.test has a lot of examples).
Pay attention to the render
function that is imported from @/pageEditor/testHelpers
. It supports Redux and Formik initialization.
See examples:
Just regular unit testing.
See examples:
This article does a good job of explaining the various ways to test hooks, including using react-testing-library's renderHook()
function.
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]);
Approach #4: use fake timers. You have to use immediate user events (click imitation) when fake timers are enabled (see here).
Example. EditorPane: instruct to use fake timers (see the beforeAll
part here), perform some actions, run the timers, assert.
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.tsx
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
})
);
When mocking RTK hooks, be careful to return a constant data reference. Otherwise, the calling the mocked hook will result in a re-render on each call.
jest.mock("@/services/api", () => ({
// BAD! - returns a different `data` reference on each calls
useGetRecipesQuery: jest.fn(() => {
data: [],
isLoading: false,
}),
});
const NO_RECIPES: readonly Recipe[] = Object.freeze([]);
jest.mock("@/services/api", () => ({
// BETTER - returns a fixed data value. But object changes reference, so be careful if caller
// uses the returned object as a dependency, e.g., in useEffect
useGetRecipesQuery: jest.fn(() => {
data: NO_RECIPES,
isLoading: false,
}),
});