Unit Testing
Testing Setup
The Hosanna UI project uses Vitest as its primary testing framework. The testing environment is configured to support TypeScript and includes necessary setup for UI component testing.
Test Configuration
The project uses the following test configuration (defined in jest.config.js
):
export default {
moduleFileExtensions: ['ts', 'tsx', 'js'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
globals: {
'ts-jest': {
tsConfig: 'tsconfig.json',
},
},
testMatch: ['**/?(*.)+(spec|test).(ts|js)?(x)'],
clearMocks: true,
};
Writing Tests
Test File Naming Convention
- Test files should be named with
.test.ts
or.test.tsx
extension - Place test files next to the component/module they are testing
- Example:
ComponentName.test.ts
for testingComponentName.ts
Basic Test Structure
import { describe, it, expect, beforeEach, vi } from 'vitest';
describe('ComponentName', () => {
beforeEach(() => {
// Setup code that runs before each test
});
it('should do something specific', () => {
// Test code
expect(result).toBe(expectedValue);
});
});
Common Testing Patterns
1. Component Testing
describe('Component', () => {
let component;
beforeEach(() => {
component = new Component();
});
it('should initialize with default values', () => {
expect(component.someProperty).toBe(defaultValue);
});
it('should handle specific actions', () => {
component.doSomething();
expect(component.state).toBe(expectedState);
});
});
2. Mocking
// Mocking functions
const mockFunction = vi.fn();
// Mocking objects
const mockObject = {
method: vi.fn(),
property: 'value'
} as unknown as OriginalType;
// Spying on methods
const spy = vi.spyOn(object, 'method');
3. Testing Async Code
it('should handle async operations', async () => {
const result = await component.asyncOperation();
expect(result).toBe(expectedValue);
});
Best Practices
-
Test Isolation
- Each test should be independent
- Use
beforeEach
to reset state - Clean up after tests if necessary
-
Meaningful Test Names
- Use descriptive test names that explain the expected behavior
- Follow the pattern: "should [expected behavior] when [condition]"
-
Test Coverage
- Test both success and error cases
- Test edge cases and boundary conditions
- Test component lifecycle methods
-
Mocking Dependencies
- Mock external dependencies
- Use dependency injection to make components testable
- Mock only what's necessary
Examples from the Codebase
1. View Testing
describe('BaseView', () => {
let view;
beforeEach(() => {
view = new BaseView();
});
it('should initialize with default values', () => {
expect(view.isFocused).toBe(false);
expect(view.visible).toBe(true);
});
it('should handle focus changes', () => {
view.isFocused = true;
expect(view.isInFocusChain()).toBe(true);
});
});
2. Transition Testing
describe('ViewTransition', () => {
let source, target, transition;
beforeEach(() => {
source = new BaseView();
target = new BaseView();
transition = new ViewTransition();
});
it('should transition between views', () => {
transition.execute(owner, source, target, true);
expect(target.visible).toBe(true);
expect(source.visible).toBe(false);
});
});
Running Tests
To run tests, use the following npm scripts:
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
Test Coverage
The project aims to maintain high test coverage. Coverage reports can be found in the coverage
directory after running tests with coverage enabled.
Key areas to focus on for testing:
- Component initialization
- State changes
- Event handling
- View transitions
- Focus management
- Error handling