乐闻世界logo
搜索文章和话题

What are the testing strategies for Expo apps? How to perform unit and end-to-end testing?

2月21日 15:19

Testing Expo apps is an important part of ensuring code quality and application stability. Expo supports multiple testing frameworks and tools, with comprehensive solutions from unit testing to end-to-end testing.

Testing Framework Selection:

  1. Jest (Unit and Integration Testing)

Jest is Expo's default testing framework, suitable for unit tests and component tests.

Installation and Configuration:

bash
npm install --save-dev jest @testing-library/react-native @testing-library/jest-native

jest.config.js Configuration:

javascript
module.exports = { preset: 'jest-expo', setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'], transformIgnorePatterns: [ 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)' ], testMatch: ['**/__tests__/**/*.test.[jt]s?(x)'], };

Unit Test Example:

typescript
// __tests__/utils.test.ts import { formatDate, calculateTotal } from '../utils'; describe('Utils', () => { describe('formatDate', () => { it('should format date correctly', () => { const date = new Date('2024-01-15'); expect(formatDate(date)).toBe('2024-01-15'); }); it('should handle null date', () => { expect(formatDate(null)).toBe(''); }); }); describe('calculateTotal', () => { it('should calculate total correctly', () => { const items = [{ price: 10 }, { price: 20 }]; expect(calculateTotal(items)).toBe(30); }); }); });

Component Test Example:

typescript
// __tests__/components/Button.test.tsx import { render, fireEvent } from '@testing-library/react-native'; import Button from '../components/Button'; describe('Button', () => { it('renders correctly', () => { const { getByText } = render(<Button title="Click me" />); expect(getByText('Click me')).toBeTruthy(); }); it('calls onPress when pressed', () => { const onPress = jest.fn(); const { getByText } = render( <Button title="Click me" onPress={onPress} /> ); fireEvent.press(getByText('Click me')); expect(onPress).toHaveBeenCalledTimes(1); }); it('disables button when disabled prop is true', () => { const { getByText } = render( <Button title="Click me" disabled /> ); const button = getByText('Click me'); expect(button.props.disabled).toBe(true); }); });
  1. Detox (End-to-End Testing)

Detox is a gray-box end-to-end testing framework suitable for testing complete user flows.

Installation and Configuration:

bash
npm install --save-dev detox detox-cli detox init

detox.config.js Configuration:

javascript
module.exports = { testRunner: { args: { '$0': 'jest', config: 'e2e/config.json', }, jest: { setupTimeout: 120000, }, }, apps: { 'ios.debug': { type: 'ios.app', binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/ExpoApp.app', build: 'xcodebuild -workspace ios/ExpoApp.xcworkspace -scheme ExpoApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build', }, 'android.debug': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..', }, }, devices: { simulator: { type: 'ios.simulator', device: { type: 'iPhone 14' }, }, emulator: { type: 'android.emulator', device: { avdName: 'Pixel_5_API_33' }, }, }, configurations: { 'ios.sim.debug': { device: 'simulator', app: 'ios.debug', }, 'android.emu.debug': { device: 'emulator', app: 'android.debug', }, }, };

End-to-End Test Example:

typescript
// e2e/login.e2e.ts describe('Login Flow', () => { beforeAll(async () => { await device.launchApp(); }); beforeEach(async () => { await device.reloadReactNative(); }); it('should login successfully with valid credentials', async () => { await element(by.id('email-input')).typeText('user@example.com'); await element(by.id('password-input')).typeText('password123'); await element(by.id('login-button')).tap(); await expect(element(by.id('welcome-screen'))).toBeVisible(); }); it('should show error with invalid credentials', async () => { await element(by.id('email-input')).typeText('invalid@example.com'); await element(by.id('password-input')).typeText('wrongpassword'); await element(by.id('login-button')).tap(); await expect(element(by.text('Invalid credentials'))).toBeVisible(); }); });
  1. React Native Testing Library

Focuses on testing user behavior, not implementation details.

Installation:

bash
npm install --save-dev @testing-library/react-native @testing-library/jest-native

Usage Example:

typescript
import { render, fireEvent, waitFor } from '@testing-library/react-native'; import LoginForm from '../components/LoginForm'; describe('LoginForm', () => { it('should submit form with valid data', async () => { const onSubmit = jest.fn(); const { getByPlaceholderText, getByText } = render( <LoginForm onSubmit={onSubmit} /> ); fireEvent.changeText( getByPlaceholderText('Email'), 'user@example.com' ); fireEvent.changeText( getByPlaceholderText('Password'), 'password123' ); fireEvent.press(getByText('Login')); await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith({ email: 'user@example.com', password: 'password123', }); }); }); });

Testing Best Practices:

  1. Testing Pyramid

    • Unit tests: 70%
    • Integration tests: 20%
    • End-to-end tests: 10%
  2. Test Naming

typescript
// Clear test descriptions describe('User Component', () => { it('should display user name when user data is provided', () => { // Test code }); it('should show loading state when fetching user data', () => { // Test code }); it('should display error message when fetch fails', () => { // Test code }); });
  1. Mock and Stub
typescript
// Mock API calls jest.mock('../api/user', () => ({ fetchUser: jest.fn(), })); import { fetchUser } from '../api/user'; describe('UserScreen', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should fetch and display user', async () => { const mockUser = { id: 1, name: 'John' }; (fetchUser as jest.Mock).mockResolvedValue(mockUser); // Test code }); });
  1. Testing Async Code
typescript
it('should handle async operations', async () => { const { getByText, findByText } = render(<AsyncComponent />); // Wait for async operation to complete await findByText('Loaded Data'); expect(getByText('Loaded Data')).toBeTruthy(); });
  1. Snapshot Testing
typescript
it('should match snapshot', () => { const tree = renderer.create(<MyComponent />).toJSON(); expect(tree).toMatchSnapshot(); });

CI/CD Integration:

  1. GitHub Actions Configuration
yaml
name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: '18' - run: npm ci - run: npm test -- --coverage - uses: codecov/codecov-action@v2
  1. Test Coverage
json
{ "collectCoverage": true, "coverageReporters": ["text", "lcov", "html"], "coverageThreshold": { "global": { "branches": 80, "functions": 80, "lines": 80, "statements": 80 } } }

Common Testing Scenarios:

  1. Navigation Testing
typescript
import { NavigationContainer } from '@react-navigation/native'; const renderWithNavigation = (component) => { return render( <NavigationContainer> {component} </NavigationContainer> ); };
  1. State Management Testing
typescript
import { renderHook, act } from '@testing-library/react-hooks'; import { useUserStore } from '../store/user'; it('should update user state', () => { const { result } = renderHook(() => useUserStore()); act(() => { result.current.setUser({ name: 'John' }); }); expect(result.current.user.name).toBe('John'); });
  1. Network Request Testing
typescript
import { rest } from 'msw'; import { setupServer } from 'msw/node'; const server = setupServer( rest.get('/api/user', (req, res, ctx) => { return res(ctx.json({ id: 1, name: 'John' })); }) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close());

By establishing a comprehensive testing system, you can significantly improve the quality and maintainability of Expo apps.

标签:Expo