7.5 KiB
7.5 KiB
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. For concrete examples, see app/products/playbooks, which is the canonical reference for test layout and patterns.
Where tests live
- Tests are co-located with source files:
*.test.tsor*.test.tsxnext to the file under test (e.g.channel.test.tsbesidechannel.ts). - Screen entry points often have an
index.test.tsxthat tests the wrapper and passes through to the main screen component. - Reference: 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])inbeforeEachandDatabaseManager.destroyServerDatabase(serverUrl)inafterEach. - 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. mockdatabase.writeoroperator.batchRecordsto throw). - Example: app/products/playbooks/actions/local/channel.test.ts, app/actions/local/post.test.ts.
Remote actions (app/actions/remote/)
- Mock
NetworkManager.getClientinbeforeAllto 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/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):
- Use a real database:
DatabaseManager.init([serverUrl])inbeforeEach, destroy inafterEach. - Set up data in the DB with the operator (e.g.
handleChannel,handleMyChannel,handleSystem) so the observe under test has real records to emit. - If the observe function depends on other observables from a mocked module (e.g.
observeConfigBooleanValuefrom./system), mock that dependency to return an Observable (e.g.jest.mocked(observeConfigBooleanValue).mockReturnValue(of$(true))) socombineLatestand similar operators receive valid observables. - Call the observe function with
database(and any ids), then subscribe with a spy:const subscriptionNext = jest.fn(); result.subscribe({ next: subscriptionNext }); - Assert the emitted value:
expect(subscriptionNext).toHaveBeenCalledWith(expectedValue).
- Use a real database:
- You can also use
firstValueFrom(observable)to await the first emission when that is clearer. - Example: app/products/playbooks/database/queries/run.test.ts (
observePlaybookRunById,observePlaybookRunProgress), app/products/playbooks/database/queries/version.test.ts (observeIsPlaybooksEnabled), app/queries/servers/channel.test.ts (observeChannelAutotranslation,observeMyChannelAutotranslation,observeIsChannelAutotranslated).
Screens and UI
- Use
renderWithEverythingfrom@test/intl-test-helperwhen the component needs database or server URL (pass{database}and optionallyserverUrl). UserenderWithIntlAndThemewhen it only needs theme/intl (no DB). - Mock heavy or external deps: navigation, remote actions,
useServerUrl, etc., withjest.mock(...). - Mocking child components : (1) Mock the module with a factory that returns only
{ __esModule: true, default: jest.fn() }—do not userequire()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) Calljest.mocked(ChildComponent).mockImplementation((props) => React.createElement(View, { testID: '...', ...props }))(orReact.createElement('ComponentName', ...)) so the implementation runs in test scope whereReactandVieware in scope. This keeps a stabletestIDand forwards props. Example: app/products/playbooks/screens/select_user/select_user.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('...')orgetAllByTestId('...')), then assert withexpect(element).toHaveProp('propName', value). Do not usejest.mocked(Component).mock.callsormock.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 inact()orwaitForwhen needed. - Use a
getBaseProps()helper typed asComponentProps<typeof Component>for default props andbeforeEach(() => jest.clearAllMocks())where appropriate. - Example: app/products/playbooks/screens/select_user/select_user.test.tsx, app/screens/show_translation/show_translation.test.tsx.
Client REST (app/client/rest/)
- Create the client (e.g.
TestHelper.createClient()), then mockclient.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/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])andDatabaseManager.destroyServerDatabase(serverUrl); getdatabaseandoperatorfromDatabaseManager.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; calllistener.remove()in cleanup.
Jest configuration
- Setup:
test/setup.tsis run before tests; avoid adding one-off mocks there. - Coverage:
coveragePathIgnorePatternsinjest.config.jsexclude/components/and/screens/, but tests for critical screens and components are still encouraged for regression safety.