5月27日 15:35

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

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