Setup and context: Why Testing Matters for Rork Max Apps
Rork Max accelerates mobile app development by generating React Native code from AI prompts and visual builders. However, AI-generated code introduces unique testing challenges: auto-generated components may have subtle bugs, dependencies shift versions rapidly, and integration points require careful validation. Production-grade apps demand comprehensive testing, even when generated.
The testing pyramid for mobile apps follows three layers:
- Unit Tests (70%) — Test individual components, hooks, and utilities in isolation
- Integration Tests (20%) — Test multiple components working together, API calls
- E2E Tests (10%) — Test complete user flows on real/simulated devices
Part 1: Jest Setup for Unit Testing
Install Testing Dependencies
Your Rork Max project likely has Jest already, but ensure you have all necessary libraries:
npm install --save-dev @testing-library/react-native @testing-library/jest-native jest-expo babel-jestOr with Yarn:
yarn add --dev @testing-library/react-native @testing-library/jest-native jest-expo babel-jestConfigure jest.config.js
Create or update jest.config.js in your project root:
module.exports = {
preset: 'jest-expo',
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testPathIgnorePatterns: ['/node_modules/', '/android/', '/ios/'],
collectCoverageFrom: [
'app/**/*.{ts,tsx}',
'!app/**/*.d.ts',
'!app/**/index.ts',
],
coverageThreshold: {
global: {
branches: 60,
functions: 60,
lines: 60,
statements: 60,
},
},
};Create jest.setup.js
import '@testing-library/jest-native/extend-expect';Run Your First Test
Create a simple test file app/__tests__/App.test.tsx:
import React from 'react';
import { render, screen } from '@testing-library/react-native';
import App from '../App';
describe('App Component', () => {
it('renders welcome message', () => {
render(<App />);
expect(screen.getByText(/Welcome/i)).toBeTruthy();
});
});Run tests:
npm testPart 2: Component Unit Testing
Testing a Button Component
Most Rork Max apps include button components. Let's test a custom button:
// app/components/Button.tsx
import React from 'react';
import { TouchableOpacity, Text } from 'react-native';
interface ButtonProps {
title: string;
onPress: () => void;
disabled?: boolean;
}
export const Button: React.FC<ButtonProps> = ({ title, onPress, disabled }) => (
<TouchableOpacity onPress={onPress} disabled={disabled}>
<Text>{title}</Text>
</TouchableOpacity>
);Now test it:
// app/__tests__/Button.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import { Button } from '../components/Button';
describe('Button Component', () => {
it('renders with correct title', () => {
render(<Button title="Press me" onPress={jest.fn()} />);
expect(screen.getByText('Press me')).toBeTruthy();
});
it('calls onPress when pressed', () => {
const mockPress = jest.fn();
render(<Button title="Press me" onPress={mockPress} />);
const button = screen.getByRole('button', { name: /press me/i });
fireEvent.press(button);
expect(mockPress).toHaveBeenCalledTimes(1);
});
it('does not call onPress when disabled', () => {
const mockPress = jest.fn();
render(
<Button title="Press me" onPress={mockPress} disabled={true} />
);
const button = screen.getByRole('button');
fireEvent.press(button);
expect(mockPress).not.toHaveBeenCalled();
});
});Testing Forms
Forms are critical in apps. Test form submission:
// app/__tests__/LoginForm.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { LoginForm } from '../components/LoginForm';
describe('LoginForm Component', () => {
it('submits form with email and password', async () => {
const mockSubmit = jest.fn();
render(<LoginForm onSubmit={mockSubmit} />);
const emailInput = screen.getByPlaceholderText('Email');
const passwordInput = screen.getByPlaceholderText('Password');
const submitButton = screen.getByText('Sign In');
fireEvent.changeText(emailInput, 'user@example.com');
fireEvent.changeText(passwordInput, 'password123');
fireEvent.press(submitButton);
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123',
});
});
});
it('shows error when email is invalid', async () => {
render(<LoginForm onSubmit={jest.fn()} />);
const emailInput = screen.getByPlaceholderText('Email');
fireEvent.changeText(emailInput, 'invalid-email');
await waitFor(() => {
expect(screen.getByText(/invalid email/i)).toBeTruthy();
});
});
});Part 3: Hook Testing with renderHook
Custom hooks are common in React Native apps. Test them with renderHook:
// app/hooks/useAuth.ts
import { useState, useCallback } from 'react';
export const useAuth = () => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const login = useCallback(async (email: string, password: string) => {
setLoading(true);
try {
// API call
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const data = await response.json();
setUser(data.user);
} finally {
setLoading(false);
}
}, []);
const logout = useCallback(() => {
setUser(null);
}, []);
return { user, loading, login, logout };
};Test the hook:
// app/__tests__/useAuth.test.ts
import { renderHook, act, waitFor } from '@testing-library/react-native';
import { useAuth } from '../hooks/useAuth';
describe('useAuth Hook', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('initializes with null user and false loading', () => {
const { result } = renderHook(() => useAuth());
expect(result.current.user).toBeNull();
expect(result.current.loading).toBe(false);
});
it('sets loading to true during login', async () => {
const { result } = renderHook(() => useAuth());
act(() => {
result.current.login('user@example.com', 'password123');
});
expect(result.current.loading).toBe(true);
});
it('updates user after successful login', async () => {
const mockUser = { id: '123', email: 'user@example.com' };
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ user: mockUser }),
})
) as jest.Mock;
const { result } = renderHook(() => useAuth());
await act(async () => {
await result.current.login('user@example.com', 'password123');
});
await waitFor(() => {
expect(result.current.user).toEqual(mockUser);
expect(result.current.loading).toBe(false);
});
});
it('clears user on logout', () => {
const { result } = renderHook(() => useAuth());
act(() => {
result.current.logout();
});
expect(result.current.user).toBeNull();
});
});Part 4: API and Service Mocking
Mocking external services (like Supabase) is essential. Use jest.mock():
// app/services/supabaseClient.ts
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL!,
process.env.EXPO_PUBLIC_SUPABASE_KEY!
);Mock it in tests:
// app/__tests__/supabaseService.test.ts
import { supabase } from '../services/supabaseClient';
jest.mock('../services/supabaseClient', () => ({
supabase: {
from: jest.fn(),
},
}));
describe('Supabase Service', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('fetches posts successfully', async () => {
const mockPosts = [
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' },
];
(supabase.from as jest.Mock).mockReturnValue({
select: jest.fn().mockResolvedValue({
data: mockPosts,
error: null,
}),
});
const { data, error } = await supabase
.from('posts')
.select('*');
expect(data).toEqual(mockPosts);
expect(error).toBeNull();
});
it('handles errors gracefully', async () => {
const mockError = { message: 'Network error' };
(supabase.from as jest.Mock).mockReturnValue({
select: jest.fn().mockResolvedValue({
data: null,
error: mockError,
}),
});
const { data, error } = await supabase
.from('posts')
.select('*');
expect(data).toBeNull();
expect(error).toEqual(mockError);
});
});Part 5: Snapshot Testing
Snapshot tests capture component output and detect regressions:
// app/__tests__/ProfileCard.snapshot.test.tsx
import React from 'react';
import { render } from '@testing-library/react-native';
import { ProfileCard } from '../components/ProfileCard';
describe('ProfileCard Snapshot', () => {
it('renders correctly', () => {
const tree = render(
<ProfileCard
name="John Doe"
bio="Developer"
avatar="https://example.com/avatar.jpg"
/>
).toJSON();
expect(tree).toMatchSnapshot();
});
});When you run npm test -- -u, Jest saves a snapshot file (ProfileCard.snapshot.test.tsx.snap). On subsequent runs, Jest compares output to the snapshot.
When to use snapshots:
- ✅ UI components with stable structure
- ✅ Regression detection for design changes
When to avoid:
- ❌ Dynamic content (timestamps, IDs)
- ❌ Frequently changing UI
Part 6: Detox E2E Setup
Detox tests apps end-to-end on real devices or simulators. Install:
npm install --save-dev detox-cli detox detox-build-configurationConfigure .detoxrc.js
Create .detoxrc.js in your project root:
module.exports = {
testRunner: 'jest',
apps: {
ios: {
type: 'ios.app',
binaryPath: 'artifacts/build/Build/Products/Release-iphonesimulator/YourApp.app',
build: 'xcodebuild -workspace ios/YourApp.xcworkspace -scheme YourApp -configuration Release -sdk iphonesimulator -derivedDataPath artifacts/build',
},
},
configurations: {
'ios.sim.debug': {
device: {
type: 'iPhone 14',
},
app: 'ios',
},
},
testRunner: 'jest',
};Build the App
detox build-framework-cache
detox build-app --configuration ios.sim.debugRun Your First E2E Test
Create e2e/firstTest.e2e.js:
describe('First E2E Test', () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('should display welcome screen', async () => {
await expect(element(by.text('Welcome'))).toBeVisible();
});
});Run the test:
detox test e2e/firstTest.e2e.js --configuration ios.sim.debugPart 7: Detox E2E Test Examples
Login Flow Test
// e2e/loginFlow.e2e.js
describe('Login Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('should successfully log in with valid credentials', async () => {
// Navigate to login screen
await element(by.id('loginButton')).multiTap();
// Enter email
await element(by.id('emailInput')).typeText('user@example.com');
// Enter password
await element(by.id('passwordInput')).typeText('password123');
// Tap sign in button
await element(by.id('signInButton')).multiTap();
// Verify successful login by checking for dashboard
await waitFor(element(by.text('Dashboard')))
.toBeVisible()
.withTimeout(5000);
});
it('should show error with invalid email', async () => {
await element(by.id('loginButton')).multiTap();
await element(by.id('emailInput')).typeText('invalid-email');
await element(by.id('signInButton')).multiTap();
await expect(element(by.text('Invalid email'))).toBeVisible();
});
});Navigation Flow Test
// e2e/navigation.e2e.js
describe('Navigation Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
it('should navigate between tabs', async () => {
// Start on home tab
await expect(element(by.text('Home'))).toBeVisible();
// Tap profile tab
await element(by.id('profileTab')).multiTap();
await expect(element(by.text('Profile'))).toBeVisible();
// Tap settings tab
await element(by.id('settingsTab')).multiTap();
await expect(element(by.text('Settings'))).toBeVisible();
// Navigate back to home
await element(by.id('homeTab')).multiTap();
await expect(element(by.text('Home'))).toBeVisible();
});
});Form Submission Test
// e2e/formSubmission.e2e.js
describe('Form Submission', () => {
beforeAll(async () => {
await device.launchApp();
});
it('should submit form with all fields filled', async () => {
// Navigate to form
await element(by.id('openFormButton')).multiTap();
// Fill form fields
await element(by.id('nameInput')).typeText('John Doe');
await element(by.id('emailInput')).typeText('john@example.com');
await element(by.id('messageInput')).typeText('Hello World');
// Submit
await element(by.id('submitButton')).multiTap();
// Verify success message
await waitFor(element(by.text('Form submitted successfully')))
.toBeVisible()
.withTimeout(5000);
});
it('should show validation error if required field is empty', async () => {
await element(by.id('openFormButton')).multiTap();
await element(by.id('emailInput')).typeText('john@example.com');
await element(by.id('submitButton')).multiTap();
await expect(element(by.text('Name is required'))).toBeVisible();
});
});Part 8: Coverage Reporting
Generate Coverage Reports
Run Jest with coverage:
npm test -- --coverageThis generates coverage reports in coverage/ directory. Coverage metrics include:
- Statements: Percentage of statements executed
- Branches: Percentage of conditional branches tested
- Functions: Percentage of functions called
- Lines: Percentage of lines executed
Set Coverage Thresholds
In jest.config.js, set minimum thresholds:
coverageThreshold: {
global: {
branches: 70,
functions: 75,
lines: 80,
statements: 80,
},
'./app/services/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90,
},
};If coverage falls below thresholds, Jest exits with code 1, failing the build.
View Coverage Report in Browser
After running coverage, open coverage/lcov-report/index.html in a browser to see detailed, interactive coverage reports.
Part 9: GitHub Actions CI Integration
Create .github/workflows/test.yml
name: Jest Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
fail_ci_if_error: falseRun on Every PR
The workflow automatically runs on pull requests. If tests fail, the PR shows a red X. If tests pass, it shows a green checkmark.

```</div></div>
---
## Part 10: E2E Tests in CI
Running Detox in CI requires Android/iOS build tools. Here's a minimal example for iOS:
```yaml
name: Detox E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build Detox
run: |
detox build-framework-cache
detox build-app --configuration ios.sim.debug
- name: Run E2E tests
run: detox test e2e --configuration ios.sim.debug --cleanupSummary: A Winning Testing Strategy for Rork Max
To ensure your Rork Max app is production-ready:
- Unit Tests (Jest): Cover components, hooks, and utilities. Aim for 80%+ line coverage.
- Component Tests: Test user interactions (button clicks, form submissions).
- Mocking: Mock external APIs and services to isolate logic.
- E2E Tests (Detox): Test critical user flows (login, payments, core features).
- CI/CD: Automate testing on every PR with GitHub Actions.
- Coverage Reports: Track and enforce coverage thresholds.
AI-generated code isn't perfect, but comprehensive testing catches issues early. Start with Jest unit tests, add E2E tests for critical paths, and iterate.
Next steps:
- Set up Jest in your Rork Max project
- Write tests for your most critical components
- Integrate GitHub Actions for continuous testing
- Gradually increase coverage as your app grows
Happy testing!