Files
iNaturalistReactNative/agent-docs/testing/integration-test-analysis.md
2026-07-15 14:59:56 -05:00

11 KiB

Integration Test Architecture and Strategy Analysis

Overview

The iNaturalistReactNative codebase separates tests into three tiers:

  • Unit tests (tests/unit/) — Isolated components, hooks, helpers with mocked dependencies
  • Integration tests (tests/integration/) — Screen-level tests exercising real Realm, real Zustand, real navigation context, with mocked API layer
  • E2E tests (e2e/) — Detox tests on real devices/simulators

This document focuses on integration tests: their intention, patterns, quality, and recommended strategy adjustments.


Intention of Integration Tests

Integration tests in this project validate cross-system behavior — verifying that screens work correctly when multiple subsystems (Realm database, Zustand stores, navigation, i18n, React Query) interact together. They sit between unit tests (isolated components) and E2E tests (full device).

What integration tests specifically validate:

  1. Data persistence flows — Observations written to Realm render correctly in lists, detail screens, and edit forms
  2. Upload lifecycle — The complete upload flow from button press through API calls to success/error status display
  3. Authentication gating — Signed-in vs signed-out users see different UI states; API calls are suppressed when signed out
  4. Offline behavior — Network failures are handled gracefully; geolocation still works offline
  5. Navigation flows — Multi-screen journeys (ObsList → ObsDetails → TaxonDetails → Explore) with real navigators
  6. Sync behavior — Deleted observations are cleaned up, updates trigger re-fetches, pull-to-refresh works
  7. Localization — Language switching persists across sign-in/sign-out cycles
  8. Error recovery — Upload failures show correct error states, then reset to actionable states

The core question they answer:

"When a user performs this action on this screen, does the data flow correctly through Realm, Zustand, API mocks, and back to the UI?"


Test Inventory

All integration tests are active and run in CI — there is no broken/ directory and jest.config.ts excludes no test paths. For the authoritative current list, run:

find tests/integration -name '*.test.js'

The suite spans three areas — screen-level tests at the top level, multi-screen flows under navigation/, and cross-system hook tests under sharedHooks/. Representative examples (not exhaustive):

File What it validates
MyObservations.test.js Upload lifecycle (batch/individual), error handling, sync, deletion cleanup
MyObservationsSimple.test.js Basic header display when signed out with unsynced observations
MyObservationsLocalization.test.js Language settings persistence across auth state changes
Explore.test.js Observations/species view switching, pull-to-refresh, filter application
ObsDetails.test.js Comment display, automatic update fetching, realm object updates
ObsEditOnline.test.js / ObsEditOffline.test.js Location display, upload progress, offline geolocation, button states
Notifications.test.js Notification display, remote photo fetching, realm persistence
SavedMatch.test.js Map visibility by latitude, Learn More button by network state
PhotoSharing.test.js File sharing integration (iOS/Android ShareMenu)
PhotoDeletion.test.js / PhotoDeletionExisting.test.js / PhotoImport.test.js Photo evidence management flows
SuggestionsWithSyncedObs.test.js / SuggestionsWithUnsyncedObs.test.js AI suggestion flows
LanguageSettings.test.js System locale vs server preference, language switching API calls
navigation/Explore.test.js Full navigation flow across 4+ screens with real navigators
navigation/AddObsButton.test.js Long-press vs press behavior, navigation dispatch payloads
navigation/AICamera.test.js, navigation/Suggestions.test.js, navigation/TaxonDetails.test.js, navigation/ObsEdit.test.js, navigation/MediaViewer.test.js, navigation/StandardCamera.test.js, navigation/SoundRecorder.test.js, navigation/PhotoLibrary.test.js, navigation/MyObservations.test.js Per-screen navigation flows
sharedHooks/useCurrentUser.test.js Hook returns signed-in user from Realm
sharedHooks/useTaxon.test.js Local/remote taxon fallback, outdated refresh, error handling
sharedHooks/useObservationsUpdates.test.js Observation update polling behavior
ObsDetailsSharedComponents/ActivityTab/ActivityHeaderContainer.test.js ID withdrawal/restoration API calls

Key Patterns

Unique Realm Setup (Boilerplate Block)

Every integration test uses a standardized setup block that creates an isolated in-memory Realm:

// UNIQUE REALM SETUP
const mockRealmIdentifier = __filename;
const { mockRealmModelsIndex, uniqueRealmBeforeAll, uniqueRealmAfterAll } = setupUniqueRealm(
  mockRealmIdentifier,
);
jest.mock( "realmModels/index", () => mockRealmModelsIndex );
jest.mock( "providers/contexts", () => {
  const originalModule = jest.requireActual( "providers/contexts" );
  return {
    __esModule: true,
    ...originalModule,
    RealmContext: {
      ...originalModule.RealmContext,
      useRealm: () => global.mockRealms[mockRealmIdentifier],
      useQuery: () => [],
    },
  };
} );
beforeAll( uniqueRealmBeforeAll );
afterAll( uniqueRealmAfterAll );
// /UNIQUE REALM SETUP

API Mocking Strategy

  • inaturalistjs methods are globally mocked in jest.setup.js with empty responses
  • Per-test, mock implementations examine request params to return contextual data:
inatjs.observations.create.mockImplementation( async ( params ) => {
  const mockObs = mockUnsyncedObservations.find( o => o.uuid === params.observation.uuid );
  return makeResponse( [{ id: faker.number.int(), uuid: mockObs.uuid }] );
} );

Rendering Approaches

  • renderAppWithComponent(<Component />) — Wraps in full app context (providers, navigation, query client)
  • renderApp() — Renders the entire App (used in navigation tests with jest.unmock("@react-navigation/native"))
  • renderAppWithObservations(obs, realmId) — Writes observations to Realm before rendering

Auth Helpers

  • signIn(user, { realm }) — Sets JWT/tokens, creates User in Realm, mocks OAuth endpoints via nock
  • signOut({ realm }) — Clears tokens, deletes all Realm objects, resets i18next

Animation/Timer Handling

  • global.withAnimatedTimeTravelEnabled() — Enables controlled animation stepping
  • global.timeTravel(ms) — Advances animation frames (used in navigation tests)
  • jest.useFakeTimers() / jest.useRealTimers() — Timer control

Strategic Recommendations

1. Clarify the Unit vs Integration Boundary

The current distinction is fuzzy in places:

  • SavedMatch.test.js and MyObservationsSimple.test.js test simple element visibility — these could be unit tests
  • sharedHooks/useCurrentUser.test.js tests a single hook return value — this is closer to a unit test
  • Meanwhile, some unit tests (e.g., useSyncObservations.test.js) test complex multi-system behavior

Recommendation: Define the boundary explicitly:

  • Unit test = Tests a single component/hook/function with ALL external dependencies mocked (including Realm, navigation, Zustand). Fast, isolated, tests implementation.
  • Integration test = Tests a screen-level component with real Realm, real Zustand, mocked API. Tests user-observable behavior across system boundaries.

2. Reduce Async Timing Fragility

Replace sleep() and custom timeout padding with deterministic patterns:

  • Use findBy* queries (which internally wait) instead of waitFor + getBy*
  • Mock timers explicitly with jest.advanceTimersByTime() instead of relying on real delays
  • For upload progress tests, consider flushing promises explicitly rather than using sleep(500) in mock implementations

3. Extract Realm Setup into a Jest Preset or Custom Environment

The 15-line boilerplate block could be:

  • A Jest setupFilesAfterFramework for integration tests with a separate Jest config
  • A test utility that handles the mock registration:
// Possible improvement
const { realm } = useIntegrationRealm(__filename);

4. Prefer Accessibility Queries Over TestIDs

Shift from getByTestId("UploadIcon.start.${uuid}") toward getByRole, getByLabelText, getByText where possible. This:

  • Validates accessibility as a side effect
  • Couples tests to user-observable behavior rather than implementation
  • Aligns with React Native Testing Library's recommended query priority

5. Consider Adding Integration Test Categories

The current flat structure makes it hard to prioritize test maintenance. Consider organizing by criticality:

tests/integration/
  critical/          # Core flows: upload, sync, auth
  screens/           # Individual screen rendering
  navigation/        # Multi-screen flows
  hooks/             # Cross-system hook behavior

6. Add a Smoke Integration Test

One test that renders the full app, signs in, and verifies all four bottom tabs render — a fast sanity check that the app boots. The existing navigation/Explore.test.js partially serves this purpose but is too complex to be a reliable smoke test.


How Unit and Integration Tests Complement Each Other

Aspect Unit Tests (tests/unit/) Integration Tests (tests/integration/)
Speed Fast (<1s each) Slow (5-50s each, 50s timeout)
Scope One component/function Full screen + Realm + Zustand
API Mocked or not called Mocked with realistic responses
Realm Global shared mock Per-file unique in-memory instance
Navigation Mocked (useRoute, useNavigation stubs) Real navigators (jest.unmock) or mocked
Catches Prop/state bugs, rendering logic, edge cases Data flow bugs, auth gating, sync issues
Maintenance Low (isolated, few dependencies) High (many systems, fragile timing)

The integration tests are most valuable when they test boundary behaviors — what happens when data crosses from API → Realm → component, or when auth state changes propagate through the app. Tests that only verify static rendering should be unit tests.


Key Files Reference

File Purpose
tests/helpers/uniqueRealm.js Creates isolated Realm per test file
tests/helpers/render.js Provider wrappers: renderAppWithComponent, renderApp, renderHookInApp
tests/helpers/user.js signIn/signOut with JWT tokens and nock mocks
tests/helpers/setStoreStateLayout.js Zustand layout state helper
tests/jest.setup.js Global mocks, animation helpers, inaturalistjs defaults
tests/jest.post-setup.js Zustand store auto-reset after each test
tests/realm.setup.js Global Realm mock (overridden by uniqueRealm in integration)
tests/factory.js Factory loader + makeResponse helper
tests/factories/ Factory definitions (Local* for Realm, Remote* for API)
__mocks__/zustand.ts Zustand reset mechanism via storeResetFns
jest.config.ts Root Jest config (transformIgnorePatterns for RN modules; no test paths are excluded)