Skip to main content

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 testing ComponentName.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

  1. Test Isolation

    • Each test should be independent
    • Use beforeEach to reset state
    • Clean up after tests if necessary
  2. Meaningful Test Names

    • Use descriptive test names that explain the expected behavior
    • Follow the pattern: "should [expected behavior] when [condition]"
  3. Test Coverage

    • Test both success and error cases
    • Test edge cases and boundary conditions
    • Test component lifecycle methods
  4. 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