Skip to content
Mehedi Hasan Sagor

// blog

Testing Hybrid Mobile Apps: Unit, Integration, and E2E Strategies for Ionic and React Native

Mehedi Hasan Sagor Nov 9, 2026 6 min read
Testing Hybrid Mobile Apps: Unit, Integration, and E2E Strategies for Ionic and React Native

Why Testing Matters for Hybrid Mobile Projects

When you invest in a single codebase that powers both an Ionic web view and a React Native native app, you gain speed and consistency—but you also inherit two very different runtime environments. A UI bug that passes in the browser can crash on iOS, and a performance regression in a native bridge can slip through a web‑only test suite. A disciplined testing strategy protects the shared business logic, validates platform‑specific integrations, and keeps release cycles predictable for busy founders and technical leads.

1. Keep the Core Logic Platform‑Neutral

The first line of defense is architectural: isolate pure JavaScript/TypeScript code from framework‑specific glue. A typical folder layout might look like this:

src/
├─ core/          # Pure business logic, services, models
├─ ionic/         # Ionic components, pages, routing
├─ native/        # React Native components, navigation
└─ shared/        # Stencil web components or utility helpers

Anything under core/ can be exercised with the same unit test runner regardless of the final platform. This reduces duplication and makes the test suite faster and cheaper to run.

2. Unit Testing the Core with Jest

Jest is the de‑facto standard for JavaScript unit testing and works equally well for both sides of the stack. Install it once at the project root:

npm i -D jest @types/jest ts-jest

Create a simple Jest config that points to the src/core folder:

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src/core'],
  moduleFileExtensions: ['ts', 'js'],
  collectCoverageFrom: ['src/core/**/*.ts'],
};

Example Test

// src/core/__tests__/priceCalculator.test.ts
import { calculateDiscount } from '../priceCalculator';

test('applies 10% discount for premium users', () => {
  const result = calculateDiscount(100, { tier: 'premium' });
  expect(result).toBe(90);
});

Because the test runs in a Node environment, it never touches the DOM, the Ionic router, or the React Native bridge. This keeps the feedback loop tight—run npm test and get results in seconds.

3. Component‑Level Tests for Each Framework

Ionic (Web) – Use Cypress Component Testing

Ionic components are essentially Angular, React, or Vue web components. Cypress now supports component testing, allowing you to mount a page or component in a real browser context.

npm i -D cypress @cypress/react
// src/ionic/__tests__/LoginForm.cy.tsx
import { mount } from '@cypress/react';
import { LoginForm } from '../components/LoginForm';

it('shows validation error when email is empty', () => {
  mount(<LoginForm />);
  cy.get('button[type="submit"]').click();
  cy.contains('Email is required').should('be.visible');
});

React Native – Use React Native Testing Library

For native UI components, @testing-library/react-native gives you a lightweight renderer that works in a Node environment.

npm i -D @testing-library/react-native jest-react-native
// src/native/__tests__/ProfileScreen.test.tsx
import { render, fireEvent } from '@testing-library/react-native';
import { ProfileScreen } from '../screens/ProfileScreen';

test('toggles dark mode switch', () => {
  const { getByTestId } = render(<ProfileScreen />);
  const switchEl = getByTestId('dark-mode-switch');
  fireEvent(switchEl, 'valueChange', true);
  expect(switchEl.props.value).toBe(true);
});

Component tests give confidence that the UI wiring works inside each framework while still keeping the core logic unit‑tested separately.

4. End‑to‑End (E2E) Tests Across Platforms

E2E tests are the safety net that verifies the whole stack—from the shared API down to the native gesture handling. Because the runtime differs, you’ll need two runners:

| Platform | Recommended Runner | Why | |----------|-------------------|-----| | Ionic (Web) | Cypress (browser) | Fast, runs in Chrome/Edge, easy CI integration | | React Native | Detox (device/simulator) | Executes on real devices, supports native gestures |

4.1 Cypress for the Ionic Build

Add a script that builds the Ionic app for the web and then launches Cypress:

"scripts": {
  "build:ionic": "ionic build",
  "cypress:run": "npm run build:ionic && cypress run"
}

A sample spec that logs in and creates an order:

// cypress/integration/order-flow.spec.js
describe('Order flow', () => {
  it('allows a guest to checkout', () => {
    cy.visit('/');
    cy.get('[data-cy=product-42]').click();
    cy.get('[data-cy=add-to-cart]').click();
    cy.get('[data-cy=checkout]').click();
    cy.get('[data-cy=guest-email]').type('test@example.com');
    cy.get('[data-cy=place-order]').click();
    cy.contains('Thank you for your purchase').should('be.visible');
  });
});

4.2 Detox for the React Native Build

Detox works with both iOS simulators and Android emulators. Install it once:

npm i -D detox jest-detox

Configure a basic Detox config in package.json:

"detox": {
  "configurations": {
    "ios.sim.debug": {
      "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/App.app",
      "type": "ios.simulator",
      "device": { "type": "iPhone 14" }
    }
  }
}

A simple test that verifies the splash screen disappears:

// e2e/firstTest.spec.js
describe('App launch', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  it('shows home screen after splash', async () => {
    await expect(element(by.id('home-screen'))).toBeVisible();
  });
});

Detox runs on real devices in CI (e.g., Bitrise, GitHub Actions) and provides flaky‑test detection out of the box.

5. Mocking Native Modules and API Calls

Both Cypress and Detox need deterministic data. Use a lightweight HTTP mocking library like msw (Mock Service Worker) for the web build, and jest-mock for native modules.

// src/mocks/handlers.ts
import { rest } from 'msw';

export const handlers = [
  rest.get('/api/products', (req, res, ctx) => {
    return res(ctx.json([{ id: 1, name: 'Demo', price: 9.99 }]));
  }),
];

Start the worker in Cypress support/index.js:

import { setupWorker } from 'msw';
import { handlers } from '../../src/mocks/handlers';

const worker = setupWorker(...handlers);
worker.start();

For React Native, replace native modules with jest mocks:

// __mocks__/react-native-fs.ts
export const readFile = jest.fn().mockResolvedValue('mocked file content');

These mocks keep tests fast and independent of external services.

6. CI/CD Integration – One Pipeline, Two Test Suites

A practical CI setup runs the three test layers in parallel:

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  test-core:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with: {node-version: '20'}
      - run: npm ci
      - run: npm test   # Jest core tests

  test-ionic:
    runs-on: ubuntu-latest
    needs: test-core
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with: {node-version: '20'}
      - run: npm ci
      - run: npm run cypress:run

  test-native:
    runs-on: macos-latest
    needs: test-core
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with: {node-version: '20'}
      - run: npm ci
      - run: npm run detox:test

The split ensures that a failure in one environment does not block the others, while still providing a single status badge for the pull request.

7. Takeaway Checklist

  • Separate concerns: keep business logic in a platform‑neutral core/ folder.
  • Unit test core with Jest – fast, no UI dependencies.
  • Component test each framework – Cypress for Ionic, React Native Testing Library for RN.
  • Run E2E on the real runtime – Cypress for web, Detox for native.
  • Mock external services with msw (web) and jest mocks (native).
  • Parallelize in CI to keep feedback under 10 minutes.

By following this layered approach, you protect the shared code that gives you a development advantage, while still catching the platform‑specific quirks that can derail a release. The result is a smoother launch cadence, lower post‑release bug churn, and a stronger confidence signal for investors and customers alike.

  • Ionic
  • React Native
  • Mobile
  • Devops
  • Testing