nexo/apps/mattermost/docs/testing_guide.md

66 lines
7.5 KiB
Markdown

# Unit testing guide
This document describes how to add and structure unit tests in the Mattermost Mobile project. For high-level testing notes and patterns, see the **Testing** section in [CLAUDE.md](../CLAUDE.md). For concrete examples, see [app/products/playbooks](../app/products/playbooks), which is the canonical reference for test layout and patterns.
## Where tests live
- Tests are **co-located** with source files: `*.test.ts` or `*.test.tsx` next to the file under test (e.g. `channel.test.ts` beside `channel.ts`).
- Screen entry points often have an `index.test.tsx` that tests the wrapper and passes through to the main screen component.
- Reference: [app/products/playbooks](../app/products/playbooks) for the full pattern.
## What to test by layer
### Local actions (`app/actions/local/`)
- Use a **real in-memory database**: `DatabaseManager.init([serverUrl])` in `beforeEach` and `DatabaseManager.destroyServerDatabase(serverUrl)` in `afterEach`.
- Use **TestHelper** for fake entities: `TestHelper.fakeChannel()`, `TestHelper.fakePost()`, `TestHelper.fakeChannelMember()`, etc.
- Cover: **not-found database** (invalid `serverUrl`), **not-found entity** (e.g. channel/post missing), **success path**, and **error path** (e.g. mock `database.write` or `operator.batchRecords` to throw).
- Example: [app/products/playbooks/actions/local/channel.test.ts](../app/products/playbooks/actions/local/channel.test.ts), [app/actions/local/post.test.ts](../app/actions/local/post.test.ts).
### Remote actions (`app/actions/remote/`)
- **Mock** `NetworkManager.getClient` in `beforeAll` to return a client with the methods used (e.g. `setChannelAutotranslation`, `setMyChannelAutotranslation`).
- Assert the client was called with the expected arguments and that the action returns `{data}` on success and `{error}` on failure.
- On error, assert `forceLogoutIfNecessary` (and any logging) is called when applicable.
- Example: [app/products/playbooks/actions/remote/playbooks.test.ts](../app/products/playbooks/actions/remote/playbooks.test.ts), [app/actions/remote/channel.test.ts](../app/actions/remote/channel.test.ts).
### Queries (`app/queries/servers/`)
- For **query** functions that return a WatermelonDB `Query`: use a real DB, create the relevant records with the operator, run the query, and assert on the fetched results.
- For **observe** functions that return RxJS observables (canonical pattern from playbooks):
1. Use a **real database**: `DatabaseManager.init([serverUrl])` in `beforeEach`, destroy in `afterEach`.
2. **Set up data** in the DB with the operator (e.g. `handleChannel`, `handleMyChannel`, `handleSystem`) so the observe under test has real records to emit.
3. If the observe function depends on other observables from a **mocked module** (e.g. `observeConfigBooleanValue` from `./system`), mock that dependency to return an Observable (e.g. `jest.mocked(observeConfigBooleanValue).mockReturnValue(of$(true))`) so `combineLatest` and similar operators receive valid observables.
4. Call the observe function with `database` (and any ids), then **subscribe** with a spy: `const subscriptionNext = jest.fn(); result.subscribe({ next: subscriptionNext });`
5. **Assert** the emitted value: `expect(subscriptionNext).toHaveBeenCalledWith(expectedValue)`.
- You can also use `firstValueFrom(observable)` to await the first emission when that is clearer.
- Example: [app/products/playbooks/database/queries/run.test.ts](../app/products/playbooks/database/queries/run.test.ts) (`observePlaybookRunById`, `observePlaybookRunProgress`), [app/products/playbooks/database/queries/version.test.ts](../app/products/playbooks/database/queries/version.test.ts) (`observeIsPlaybooksEnabled`), [app/queries/servers/channel.test.ts](../app/queries/servers/channel.test.ts) (`observeChannelAutotranslation`, `observeMyChannelAutotranslation`, `observeIsChannelAutotranslated`).
### Screens and UI
- Use **`renderWithEverything`** from `@test/intl-test-helper` when the component needs database or server URL (pass `{database}` and optionally `serverUrl`). Use **`renderWithIntlAndTheme`** when it only needs theme/intl (no DB).
- Mock heavy or external deps: navigation, remote actions, `useServerUrl`, etc., with `jest.mock(...)`.
- **Mocking child components** : (1) Mock the module with a factory that returns only `{ __esModule: true, default: jest.fn() }`—do **not** use `require()` or other out-of-scope variables inside the factory, or Jest will throw. (2) Import the child component (it will be the mocked reference after hoisting). (3) Call `jest.mocked(ChildComponent).mockImplementation((props) => React.createElement(View, { testID: '...', ...props }))` (or `React.createElement('ComponentName', ...)`) so the implementation runs in test scope where `React` and `View` are in scope. This keeps a stable `testID` and forwards props. Example: [app/products/playbooks/screens/select_user/select_user.test.tsx](../app/products/playbooks/screens/select_user/select_user.test.tsx), [app/products/playbooks/screens/participant_playbooks/participant_playbooks.test.tsx](../app/products/playbooks/screens/participant_playbooks/participant_playbooks.test.tsx).
- **Asserting on props passed to children:** Query the **screen** for the rendered element (e.g. `getByTestId('...')` or `getAllByTestId('...')`), then assert with `expect(element).toHaveProp('propName', value)`. **Do not** use `jest.mocked(Component).mock.calls` or `mock.calls[0][0]` to inspect props—assert on what is in the tree, as in playbooks screen tests.
- Assert that key elements render (e.g. by `testID`) and that user actions (e.g. toggle, button press) call the expected handlers or actions.
- Use `fireEvent.press()` for button/toggle interactions; wrap async updates in `act()` or `waitFor` when needed.
- Use a **`getBaseProps()`** helper typed as `ComponentProps<typeof Component>` for default props and `beforeEach(() => jest.clearAllMocks())` where appropriate.
- Example: [app/products/playbooks/screens/select_user/select_user.test.tsx](../app/products/playbooks/screens/select_user/select_user.test.tsx), [app/screens/show_translation/show_translation.test.tsx](../app/screens/show_translation/show_translation.test.tsx).
### Client REST (`app/client/rest/`)
- Create the client (e.g. `TestHelper.createClient()`), then mock `client.doFetch`.
- Assert the request URL (and query string if any) and options (method, body, etc.), and that the return value or thrown error matches expectations.
- Example: [app/products/playbooks/client/rest.test.ts](../app/products/playbooks/client/rest.test.ts), [app/client/rest/channels.test.ts](../app/client/rest/channels.test.ts).
## Helpers and setup
- **TestHelper** (`test/test_helper.ts`): `fakeChannel`, `fakePost`, `fakeUser`, `fakeChannelMember`, etc.; `createClient()` for REST client tests.
- **Database**: `DatabaseManager.init([serverUrl])` and `DatabaseManager.destroyServerDatabase(serverUrl)`; get `database` and `operator` from `DatabaseManager.getServerDatabaseAndOperator(serverUrl)`.
- **Rendering**: `renderWithEverything(ui, { database, serverUrl })` when the component needs DB or server context.
- **Events**: `DeviceEventEmitter.addListener(Events.SOME_EVENT, callback)` to assert that an event was emitted; call `listener.remove()` in cleanup.
## Jest configuration
- Setup: `test/setup.ts` is run before tests; avoid adding one-off mocks there.
- Coverage: `coveragePathIgnorePatterns` in `jest.config.js` exclude `/components/` and `/screens/`, but tests for critical screens and components are still encouraged for regression safety.