meld
Version:
Meld: A template language for LLM prompts
421 lines (330 loc) • 10.8 kB
Markdown
# Testing in Meld
This document outlines the testing infrastructure and best practices for the Meld codebase. It serves as a practical guide for writing and maintaining tests.
## Directory Structure
Tests are organized following these conventions:
```
project-root/
├─ tests/ # Test infrastructure and shared resources
│ ├─ utils/ # Test utilities and factories
│ ├─ mocks/ # Shared mock implementations
│ ├─ fixtures/ # Test fixture data
│ └─ setup.ts # Global test setup
└─ services/ # Service implementations with co-located tests
└─ ServiceName/
├─ ServiceName.test.ts # Unit tests
├─ ServiceName.integration.test.ts # Integration tests
└─ handlers/
└─ HandlerName.test.ts # Handler-specific tests
```
## Test Infrastructure
### Core Testing Utilities
1. **TestContext**
- Central test harness providing access to all test utilities
- Manages test state and cleanup
- Available globally in tests as `testContext`
2. **Test Factories**
- Located in `tests/utils/testFactories.ts`
- Provides helper functions for creating test nodes and mocks
- Ensures consistent test data creation
Example usage:
```typescript
import {
createDefineDirective,
createLocation,
createMockStateService
} from '@tests/utils/testFactories';
const node = createDefineDirective(
'greet',
'echo "Hello"',
[],
createLocation(1, 1, 1, 20)
);
const mockState = createMockStateService();
```
### Mock Services
The test factories provide mock implementations for all core services:
```typescript
const mockServices = {
stateService: createMockStateService(),
validationService: createMockValidationService(),
resolutionService: createMockResolutionService(),
fileSystemService: createMockFileSystemService(),
// ... etc
};
```
Each mock service implements the corresponding interface and provides sensible defaults.
## Writing Tests
### Service Tests
Service tests should follow this structure:
```typescript
describe('ServiceName', () => {
let service: ServiceName;
let dependencies: {
stateService: IStateService;
// ... other dependencies
};
beforeEach(() => {
dependencies = {
stateService: createMockStateService(),
// ... initialize other dependencies
};
service = new ServiceName(dependencies);
});
describe('core functionality', () => {
it('should handle basic operations', async () => {
// Arrange
const input = // ... test input
// Act
const result = await service.operation(input);
// Assert
expect(result).toBeDefined();
expect(dependencies.stateService.someMethod)
.toHaveBeenCalledWith(expectedArgs);
});
});
describe('error handling', () => {
it('should handle errors appropriately', async () => {
// ... error test cases
});
});
});
```
### Directive Handler Tests
Directive handler tests should cover:
1. Value Processing
- Basic value handling
- Parameter processing
- Edge cases
2. Validation Integration
- Integration with ValidationService
- Validation error handling
3. State Management
- State updates
- Command/variable storage
- Original and transformed node states
- Node replacement handling
4. Transformation Behavior
- Node replacement generation
- Transformation state preservation
- Clean output verification
5. Error Handling
- Validation errors
- Resolution errors
- State errors
- Transformation errors
Example structure:
```typescript
describe('HandlerName', () => {
let handler: HandlerName;
let dependencies: {
validationService: IValidationService;
stateService: IStateService;
resolutionService: IResolutionService;
};
beforeEach(() => {
dependencies = {
validationService: createMockValidationService(),
stateService: createMockStateService(),
resolutionService: createMockResolutionService()
};
handler = new HandlerName(dependencies);
});
describe('value processing', () => {
// Value processing tests
});
describe('validation', () => {
// Validation tests
});
describe('state management', () => {
// State management tests
});
describe('transformation', () => {
it('should provide correct replacement nodes', async () => {
const node = createDirectiveNode('test', { value: 'example' });
const result = await handler.execute(node, context);
expect(result.replacement).toBeDefined();
expect(result.replacement.type).toBe('Text');
expect(result.replacement.content).toBe('example');
});
it('should preserve location in transformed nodes', async () => {
const node = createDirectiveNode('test', { value: 'example' });
const result = await handler.execute(node, context);
expect(result.replacement.location).toEqual(node.location);
});
});
describe('error handling', () => {
// Error handling tests
});
});
```
### Integration Tests
Integration tests should focus on real-world scenarios and service interactions:
```typescript
describe('Service Integration', () => {
let services: {
directiveService: DirectiveService;
stateService: StateService;
// ... other real service instances
};
beforeEach(() => {
services = {
directiveService: new DirectiveService(),
stateService: new StateService(),
// ... initialize other services
};
// Initialize service relationships
services.directiveService.initialize(
services.validationService,
services.stateService,
services.resolutionService
);
});
it('should process complex scenarios', async () => {
// Test end-to-end flows
});
it('should generate clean output without directives', async () => {
const input = `
@text greeting = "Hello"
@run [echo ${greeting}]
Regular text
`;
const result = await processDocument(input);
expect(result).not.toContain('@text');
expect(result).not.toContain('@run');
expect(result).toContain('Hello');
expect(result).toContain('Regular text');
});
it('should maintain both original and transformed states', async () => {
const state = await processDocument(input);
expect(state.getOriginalNodes()).toHaveLength(3);
expect(state.getTransformedNodes()).toHaveLength(2);
});
});
```
## Best Practices
1. **Test Organization**
- Co-locate tests with implementation files
- Use clear, descriptive test names
- Group related tests using `describe` blocks
- Follow the Arrange-Act-Assert pattern
2. **Mock Usage**
- Use the provided mock factories
- Set up specific mock implementations in beforeEach
- Clear all mocks between tests
- Be explicit about mock expectations
3. **Error Testing**
- Test both expected and unexpected errors
- Verify error messages and types
- Test error propagation
- Include location information in errors
4. **Location Handling**
- Always include location information in test nodes
- Use `createLocation` helper for consistency
- Test location propagation in errors
- Verify location preservation in transformed nodes
5. **State Management**
- Test state immutability
- Verify state cloning
- Test parent/child state relationships
- Validate state updates
- Test both original and transformed node states
- Verify transformation state persistence
6. **Transformation Testing**
- Test node replacement generation
- Verify clean output formatting
- Test transformation state inheritance
- Validate directive removal in output
- Test complex transformation scenarios
## Running Tests
```bash
# Run all tests
npm test
# Run specific test file
npm test services/DirectiveService/handlers/definition/DefineDirectiveHandler.test.ts
# Run tests in watch mode
npm test -- --watch
# Run tests with coverage
npm test -- --coverage
# Run tests with different verbosity levels
MELD_TEST_VERBOSE=true npm test
MELD_TEST_OUTPUT_LEVEL=minimal npm test
```
### Controlling Test Output Verbosity
The Meld testing infrastructure includes a selective test output system that allows for precise control over test verbosity. This is especially useful when debugging complex test failures or when running tests in CI environments.
#### Global Test Output Control
You can control the verbosity of all tests using environment variables:
```bash
# Enable verbose output for all tests
MELD_TEST_VERBOSE=true npm test
# Set a specific output level for all tests
MELD_TEST_OUTPUT_LEVEL=minimal npm test
MELD_TEST_OUTPUT_LEVEL=normal npm test
MELD_TEST_OUTPUT_LEVEL=verbose npm test
MELD_TEST_OUTPUT_LEVEL=debug npm test
```
#### Per-Test Output Control
For more fine-grained control, you can configure output for specific tests:
```typescript
import { withTestOutput } from '@tests/utils/debug/vitest-output-setup';
describe('My test suite', () => {
it('should test with custom output', async () => {
await withTestOutput({ level: 'verbose' }, async () => {
// Your test code here
// Will run with verbose output regardless of global settings
});
});
});
```
#### Filtering Specific Operations or State Fields
You can also filter specific operations or state fields:
```typescript
await withTestOutput({
level: 'verbose',
include: ['state.variables', 'resolution.process'],
exclude: ['validation.details']
}, async () => {
// Will only show state variables and resolution process
// Will exclude validation details
});
```
#### Debugging Failed Tests
When debugging complex test failures:
```bash
# Run a specific failing test with maximum verbosity
MELD_TEST_VERBOSE=true npm test path/to/failing.test.ts
# OR use targeted verbosity in the test itself
it('failing test case', async () => {
await withTestOutput({
level: 'debug',
include: ['state', 'resolution', 'transformation']
}, async () => {
// Test code here with detailed output
});
});
```
## Test Coverage
The project maintains high test coverage through:
- Unit tests for all services and handlers
- Integration tests for service interactions
- Error case coverage
- Edge case testing
Coverage reports can be generated using:
```bash
npm test -- --coverage
```
## Debugging Tests
1. Use the `debug` logger in tests:
```typescript
import { debug } from '@core/utils/logger';
it('should handle complex case', () => {
debug('Test state:', someObject);
});
```
2. Use Node.js debugger:
- Add `debugger` statement in test
- Run `npm test -- --inspect-brk`
- Connect Chrome DevTools
3. Use Vitest UI:
```bash
npm test -- --ui
```