Testing
FitFileViewer uses Vitest for testing with comprehensive test coverage.
Test Structureβ
tests/
βββ unit/ # Unit tests
β βββ formatDistance.test.js
β βββ formatDuration.test.js
β βββ ...
βββ integration/ # Integration tests
β βββ fileLoading.test.js
β βββ ...
βββ e2e/ # End-to-end tests
βββ ...
Running Testsβ
# Run all tests
npm test
# Run with coverage
npm run test:coverage
# Run in watch mode
npm run test:watch
# Run specific file
npm test -- tests/unit/formatDistance.test.js
# Run with pattern
npm test -- --grep "formatDistance"
Writing Testsβ
Basic Test Structureβ
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { formatDistance } from '../../utils/formatting/formatDistance.js';
describe('formatDistance', () => {
describe('basic formatting', () => {
it('should format meters to kilometers', () => {
expect(formatDistance(5000)).toBe('5.00 km');
});
it('should handle decimal values', () => {
expect(formatDistance(5500)).toBe('5.50 km');
});
});
describe('edge cases', () => {
it('should handle zero', () => {
expect(formatDistance(0)).toBe('0.00 km');
});
it('should handle negative values', () => {
expect(formatDistance(-1000)).toBe('-1.00 km');
});
});
});
Testing with Setup/Teardownβ
describe('StateManager', () => {
let stateManager;
beforeEach(() => {
stateManager = new StateManager();
});
afterEach(() => {
stateManager.reset();
});
it('should store values', () => {
stateManager.set('key', 'value');
expect(stateManager.get('key')).toBe('value');
});
});
Testing Async Functionsβ
describe('loadFile', () => {
it('should load file successfully', async () => {
const result = await loadFile('test.fit');
expect(result).toBeDefined();
expect(result.records).toBeInstanceOf(Array);
});
it('should reject invalid files', async () => {
await expect(loadFile('invalid.txt'))
.rejects.toThrow('Invalid file type');
});
});
Mockingβ
import { vi } from 'vitest';
describe('with mocks', () => {
it('should call callback', () => {
const callback = vi.fn();
processData(data, callback);
expect(callback).toHaveBeenCalled();
expect(callback).toHaveBeenCalledWith(expectedData);
});
});
Test Categoriesβ
Unit Testsβ
Test individual functions in isolation:
// Test pure functions
describe('calculateAverage', () => {
it('should calculate average', () => {
expect(calculateAverage([1, 2, 3])).toBe(2);
});
});
Integration Testsβ
Test module interactions:
// Test components working together
describe('FileProcessor', () => {
it('should parse and format file data', async () => {
const fileData = await loadFile('test.fit');
const formatted = formatFileData(fileData);
expect(formatted.summary).toBeDefined();
expect(formatted.records.length).toBeGreaterThan(0);
});
});
E2E Testsβ
Test full user workflows:
// Test complete user scenarios
describe('User opens FIT file', () => {
it('should display data in all tabs', async () => {
await openFile('activity.fit');
await clickTab('Map');
expect(await isMapVisible()).toBe(true);
await clickTab('Charts');
expect(await areChartsVisible()).toBe(true);
});
});
Coverageβ
View Coverage Reportβ
npm run test:coverage
Coverage Targetsβ
| Type | Target |
|---|---|
| Statements | 80%+ |
| Branches | 75%+ |
| Functions | 80%+ |
| Lines | 80%+ |
Coverage Configurationβ
// vitest.config.js
export default {
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
exclude: ['tests/', 'node_modules/']
}
}
};
Best Practicesβ
Doβ
- β Test one thing per test
- β Use descriptive test names
- β Test edge cases
- β Test error conditions
- β Keep tests independent
Don'tβ
- β Test implementation details
- β Use magic numbers
- β Leave console.log in tests
- β Depend on test order
Test Namingβ
// Good: Describes behavior
it('should return formatted string when given valid input', () => {});
it('should throw error when input is null', () => {});
// Bad: Vague
it('works', () => {});
it('test1', () => {});
Next: Build & Release β