Skip to main content

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​

TypeTarget
Statements80%+
Branches75%+
Functions80%+
Lines80%+

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 β†’