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

Expo应用的测试策略有哪些?如何进行单元测试和端到端测试?

2月21日 15:19

Expo应用的测试是确保代码质量和应用稳定性的重要环节。Expo支持多种测试框架和工具,从单元测试到端到端测试都有完善的解决方案。

测试框架选择:

  1. Jest(单元测试和集成测试)

Jest是Expo默认的测试框架,适合单元测试和组件测试。

安装和配置:

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

jest.config.js配置:

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)'], };

单元测试示例:

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); }); }); });

组件测试示例:

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(端到端测试)

Detox是灰盒端到端测试框架,适合测试完整的用户流程。

安装和配置:

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

detox.config.js配置:

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', }, }, };

端到端测试示例:

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

专注于测试用户行为,而不是实现细节。

安装:

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

使用示例:

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', }); }); }); });

测试最佳实践:

  1. 测试金字塔

    • 单元测试:70%
    • 集成测试:20%
    • 端到端测试:10%
  2. 测试命名

typescript
// 清晰的测试描述 describe('User Component', () => { it('should display user name when user data is provided', () => { // 测试代码 }); it('should show loading state when fetching user data', () => { // 测试代码 }); it('should display error message when fetch fails', () => { // 测试代码 }); });
  1. Mock和Stub
typescript
// Mock API调用 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); // 测试代码 }); });
  1. 测试异步代码
typescript
it('should handle async operations', async () => { const { getByText, findByText } = render(<AsyncComponent />); // 等待异步操作完成 await findByText('Loaded Data'); expect(getByText('Loaded Data')).toBeTruthy(); });
  1. 快照测试
typescript
it('should match snapshot', () => { const tree = renderer.create(<MyComponent />).toJSON(); expect(tree).toMatchSnapshot(); });

CI/CD集成:

  1. GitHub Actions配置
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. 测试覆盖率
json
{ "collectCoverage": true, "coverageReporters": ["text", "lcov", "html"], "coverageThreshold": { "global": { "branches": 80, "functions": 80, "lines": 80, "statements": 80 } } }

常见测试场景:

  1. 导航测试
typescript
import { NavigationContainer } from '@react-navigation/native'; const renderWithNavigation = (component) => { return render( <NavigationContainer> {component} </NavigationContainer> ); };
  1. 状态管理测试
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. 网络请求测试
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());

通过建立完善的测试体系,可以显著提高Expo应用的质量和可维护性。

标签:Expo