Unit testing specification
Unit testing is a crucial means of ensuring code quality. Through standardized unit testing practices, defects can be effectively reduced, and code maintainability can be improved. In the frontend domain, the diverse technology stack requires unit testing to be tailored to framework features and business scenarios.
Core Value of Unit Testing
Unit testing verifies the smallest testable units of code, typically functions or class methods. Well-designed unit testing offers three key benefits:
- Rapid Feedback: Immediately validates logic correctness after code changes, significantly improving efficiency compared to manual testing.
- Design Improvement: Writing tests naturally encourages code decoupling and enhances modularity.
- Documentation Role: Test cases serve as living documentation for code behavior.
// Example: Testing pure functions
function add(a, b) {
return a + b
}
describe('add function', () => {
it('should return 5 when adding 2 and 3', () => {
expect(add(2, 3)).toBe(5)
})
it('should handle negative numbers', () => {
expect(add(-1, -1)).toBe(-2)
})
})
Frontend Unit Testing Framework Selection
Mainstream frontend testing frameworks have distinct focuses:
- Jest: A zero-configuration solution by Facebook, featuring built-in assertion libraries and coverage tools.
- Vitest: A Vite-based ultra-fast testing solution compatible with Jest syntax.
- Mocha + Chai: A flexible combination suitable for highly customized scenarios.
// Vitest example
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './Button.vue'
describe('Button Component', () => {
it('renders slot content', () => {
const wrapper = mount(Button, {
slots: { default: 'Click Me' }
})
expect(wrapper.text()).toContain('Click Me')
})
})
Test Coverage Standards
Coverage metrics should include:
- Line Coverage: At least 80%.
- Branch Coverage: All conditional branches must be tested.
- Function Coverage: All exported functions should be tested.
Configure thresholds via .nycrc
:
{
"check-coverage": true,
"lines": 80,
"branches": 75,
"functions": 85,
"statements": 80
}
Best Practices for Component Testing
UI component testing should focus on:
- Rendering Verification: Check DOM structure and style classes.
- Interaction Testing: Simulate user-triggered events.
- Props Validation: Test behavior under different prop combinations.
// React component testing example
import { render, screen, fireEvent } from '@testing-library/react'
import Toggle from './Toggle'
test('toggles state when clicked', () => {
render(<Toggle />)
const button = screen.getByRole('switch')
expect(button).toHaveAttribute('aria-checked', 'false')
fireEvent.click(button)
expect(button).toHaveAttribute('aria-checked', 'true')
})
Patterns for Asynchronous Code Testing
Three approaches to handle asynchronous logic:
- Callback Detection: Use the
done
parameter. - Promise Return: Let the testing framework wait automatically.
- Async/Await: The most intuitive modern approach.
// Asynchronous testing example
describe('fetchUser API', () => {
it('resolves with user data', async () => {
const user = await fetchUser(123)
expect(user).toHaveProperty('id', 123)
})
it('rejects when user not found', () => {
await expect(fetchUser(999)).rejects.toThrow('Not Found')
})
})
Test Data Management Strategies
Test data organization directly impacts maintainability:
- Factory Functions: Dynamically generate test data.
- Fixed Fixtures: Store static data in JSON files.
- Random Data: Use faker.js to generate realistic data.
// Factory function example
const createProduct = (overrides = {}) => ({
id: faker.datatype.uuid(),
name: faker.commerce.productName(),
price: faker.commerce.price(),
...overrides
})
describe('Shopping Cart Logic', () => {
it('should calculate total price including item prices', () => {
const cart = {
items: [createProduct({ price: 100 }), createProduct({ price: 200 })]
}
expect(calculateTotal(cart)).toBe(300)
})
})
Test Performance Optimization
Speed-up solutions for large projects:
- Parallel Execution: Use Jest's
--maxWorkers
parameter. - File Filtering: Run only tests related to modified files.
- Mock Optimization: Replace full implementations with lightweight mocks.
# Parallel test execution
jest --maxWorkers=4
# Run only tests related to modified files
jest --onlyChanged
Testing in Continuous Integration
Key CI pipeline configurations:
- Caching Strategy: Cache
node_modules
and Jest cache directories. - Phased Execution: Separate unit tests from E2E tests.
- Failure Retry: Automatically retry flaky tests.
# GitHub Actions example
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/cache@v3
with:
path: |
**/node_modules
**/.jest-cache
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- run: yarn test:unit --ci --runInBand
Avoiding Common Anti-Patterns
Testing practices to avoid:
- Over-Implementation Verification: Testing internal logic instead of behavior.
- Fragile Selectors: Relying on DOM structure instead of semantic attributes.
- Global State Leaks: Failing to reset state between tests.
// Anti-pattern example: Testing implementation details
test('should not test internal state directly', () => {
const instance = new Component()
expect(instance._internalState).toBe(0) // Bad practice
// Should test public behavior instead
instance.doSomething()
expect(instance.getResult()).toBe(1)
})
Principles for Maintaining Test Code
Key points for keeping test code quality high:
- DRY Principle: Reuse common logic via setup functions.
- Clear Descriptions: Test descriptions should include expected behavior and context.
- Layered Organization: Structure test files by functional domains.
// Test organization example
src/
components/
Button/
Button.tsx
Button.test.tsx # Unit tests
Button.stories.tsx # Visual cases
utils/
math.test.ts # Pure function tests
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn