Testing
Guide for running and writing tests.
Running Tests
All Tests
bash
npm testSpecific Test File
bash
npm test -- src/fields/BaseField.test.tsWatch Mode
bash
npm run test:watchCoverage Report
bash
npm run test:coverageOpens HTML coverage report in browser.
Type Checking
bash
npm run type-checkTest Structure
Directory Layout
tests/
├── unit/
│ ├── fields.test.ts
│ ├── filters.test.ts
│ ├── fieldConditions.test.ts
│ ├── formatters.test.ts
│ ├── columns.test.ts
│ ├── beautify.test.ts
│ └── exceptions.test.ts
└── integration/
├── stockScreener.test.ts
├── cryptoScreener.test.ts
└── otherScreeners.test.tsWriting Unit Tests
Basic Test
typescript
import { describe, it, expect } from 'vitest';
import { BaseField } from '../src/fields/BaseField';
describe('BaseField', () => {
it('should create field with metadata', () => {
const field = new BaseField('TEST', {
label: 'Test Field',
fieldName: 'test_field',
format: 'float',
interval: false,
historical: false,
});
expect(field.name).toBe('TEST');
expect(field.metadata.label).toBe('Test Field');
});
});Testing Comparison Operators
typescript
describe('Comparison Operators', () => {
const field = new BaseField('PRICE', metadata);
it('should create gt condition', () => {
const condition = field.gt(100);
expect(condition.field).toBe(field);
expect(condition.operation).toBe(FilterOperator.GREATER);
expect(condition.value).toBe(100);
});
it('should create between condition', () => {
const condition = field.between(10, 500);
expect(condition.operation).toBe(FilterOperator.IN_RANGE);
expect(condition.value).toEqual([10, 500]);
});
});Testing Field Modifiers
typescript
describe('Field Modifiers', () => {
it('should create field with interval', () => {
const field = new BaseField('PRICE', metadata);
const modified = field.withInterval('1W');
expect(modified).toBeInstanceOf(FieldWithInterval);
expect(modified.interval).toBe('1W');
expect(modified.baseField).toBe(field);
});
it('should create field with history', () => {
const field = new BaseField('PRICE', metadata);
const modified = field.withHistory(30);
expect(modified).toBeInstanceOf(FieldWithHistory);
expect(modified.bars).toBe(30);
});
});Testing Filters
typescript
describe('Filter', () => {
it('should convert to dict', () => {
const filter = new Filter('close', FilterOperator.GREATER, 100);
const dict = filter.toDict();
expect(dict).toEqual({
left: 'close',
operation: 'greater',
right: 100,
});
});
it('should handle range filters', () => {
const filter = new Filter(
'market_cap_basic',
FilterOperator.IN_RANGE,
[1e9, 10e9]
);
const dict = filter.toDict();
expect(dict.right).toEqual([1e9, 10e9]);
});
});Testing Formatters
typescript
describe('Formatters', () => {
describe('millify', () => {
it('should format thousands', () => {
expect(millify(1000)).toBe('1K');
expect(millify(1500)).toBe('1.5K');
});
it('should format millions', () => {
expect(millify(1_000_000)).toBe('1M');
expect(millify(1_234_567)).toBe('1.23M');
});
it('should format billions', () => {
expect(millify(1_000_000_000)).toBe('1B');
});
it('should handle negative numbers', () => {
expect(millify(-1_500_000)).toBe('-1.5M');
});
});
describe('formatCurrency', () => {
it('should format with default symbol', () => {
expect(formatCurrency(1234.56)).toBe('$1,234.56');
});
it('should format with custom symbol', () => {
expect(formatCurrency(1234.56, '€')).toBe('€1,234.56');
});
});
});Writing Integration Tests
Mock HTTP Requests
typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { StockScreener, StockField } from '../src';
describe('StockScreener Integration', () => {
let screener: StockScreener;
beforeEach(() => {
screener = new StockScreener();
});
it('should fetch stock data', async () => {
const mockData = {
data: [
{
symbol: 'AAPL',
name: 'Apple Inc.',
close: 150.25,
volume: 50_000_000,
},
],
totalCount: 1,
};
// Mock the HTTP request
vi.spyOn(screener as any, '_makeRequest').mockResolvedValue(mockData);
screener
.where(StockField.PRICE.gt(100))
.select(StockField.NAME, StockField.PRICE, StockField.VOLUME);
const results = await screener.get();
expect(results.data).toEqual(mockData.data);
expect(results.totalCount).toBe(1);
});
it('should handle errors', async () => {
const mockError = new MalformedRequestException(
400,
'Invalid request',
'https://api.example.com',
'{}'
);
vi.spyOn(screener as any, '_makeRequest').mockRejectedValue(mockError);
await expect(screener.get()).rejects.toThrow(MalformedRequestException);
});
});Testing Multiple Screeners
typescript
describe('All Screeners', () => {
it('should work with StockScreener', async () => {
const screener = new StockScreener();
vi.spyOn(screener as any, '_makeRequest').mockResolvedValue({
data: [],
totalCount: 0,
});
const results = await screener.get();
expect(results).toHaveProperty('data');
});
it('should work with CryptoScreener', async () => {
const screener = new CryptoScreener();
vi.spyOn(screener as any, '_makeRequest').mockResolvedValue({
data: [],
totalCount: 0,
});
const results = await screener.get();
expect(results).toHaveProperty('data');
});
});Testing Async Code
Testing Promises
typescript
it('should fetch data asynchronously', async () => {
const screener = new StockScreener();
const promise = screener.get();
await expect(promise).resolves.toHaveProperty('data');
await expect(promise).resolves.toHaveProperty('totalCount');
});Testing Async Generators
typescript
it('should stream data', async () => {
const screener = new StockScreener();
vi.spyOn(screener as any, '_makeRequest').mockResolvedValue({
data: [{ symbol: 'AAPL', close: 150 }],
totalCount: 1,
});
const results = [];
let count = 0;
for await (const data of screener.stream({ maxIterations: 3 })) {
if (data) {
results.push(data);
}
count++;
if (count >= 3) break;
}
expect(results.length).toBeGreaterThan(0);
});Test Utilities
Setup and Teardown
typescript
describe('Screener Tests', () => {
let screener: StockScreener;
beforeEach(() => {
// Setup before each test
screener = new StockScreener();
});
afterEach(() => {
// Cleanup after each test
vi.clearAllMocks();
});
it('should run test', () => {
// Test code
});
});Shared Test Data
typescript
const mockStockData = {
symbol: 'AAPL',
name: 'Apple Inc.',
close: 150.25,
volume: 50_000_000,
market_cap_basic: 2_500_000_000_000,
};
const mockFieldMetadata: FieldMetadata = {
label: 'Test Field',
fieldName: 'test_field',
format: 'float',
interval: false,
historical: false,
};
describe('Tests', () => {
it('should use mock data', () => {
expect(mockStockData.symbol).toBe('AAPL');
});
});Testing Edge Cases
Boundary Values
typescript
describe('Edge Cases', () => {
it('should handle zero', () => {
expect(millify(0)).toBe('0');
});
it('should handle very large numbers', () => {
expect(millify(1_234_567_890_123)).toBe('1.23T');
});
it('should handle very small numbers', () => {
expect(formatFloat(0.001, 3)).toBe('0.001');
});
it('should handle negative numbers', () => {
expect(millify(-1_500_000)).toBe('-1.5M');
});
});Error Conditions
typescript
describe('Error Handling', () => {
it('should throw on invalid field', () => {
expect(() => {
new BaseField('', metadata);
}).toThrow();
});
it('should validate field-to-field comparison', () => {
const field1 = new BaseField('PRICE', metadata);
const field2 = new BaseField('VOLUME', metadata);
expect(() => {
new FieldCondition(field1, FilterOperator.GREATER, field2);
}).toThrow('Field-to-field comparisons are not supported');
});
});Null/Undefined Values
typescript
describe('Null Handling', () => {
it('should handle null gracefully', () => {
const result = formatValue(null);
expect(result).toBe('N/A');
});
it('should handle undefined', () => {
const result = formatValue(undefined);
expect(result).toBe('N/A');
});
});Snapshot Testing
typescript
describe('Snapshot Tests', () => {
it('should match filter snapshot', () => {
const filter = new Filter('close', FilterOperator.GREATER, 100);
expect(filter.toDict()).toMatchSnapshot();
});
it('should match formatted output', () => {
const formatted = formatCurrency(1234.56);
expect(formatted).toMatchSnapshot();
});
});Performance Testing
typescript
describe('Performance', () => {
it('should complete within time limit', () => {
const start = Date.now();
// Perform operation
for (let i = 0; i < 10000; i++) {
millify(i * 1000);
}
const duration = Date.now() - start;
expect(duration).toBeLessThan(1000); // Should complete in < 1 second
});
});Coverage Goals
Minimum Coverage
- Statements: 80%
- Branches: 75%
- Functions: 80%
- Lines: 80%
View Coverage Report
bash
npm run test:coverageOpens HTML report showing:
- Covered/uncovered lines
- Branch coverage
- Function coverage
- File-by-file breakdown
Coverage by File
File | % Stmts | % Branch | % Funcs | % Lines
----------------------|---------|----------|---------|--------
BaseField.ts | 95.23 | 91.67 | 100.00 | 95.00
Filter.ts | 98.41 | 95.83 | 100.00 | 98.21
BaseScreener.ts | 88.57 | 78.26 | 92.31 | 88.24
formatters.ts | 96.55 | 93.75 | 100.00 | 96.43Continuous Integration
Tests run automatically on:
- Pull requests
- Commits to main
- Release tags
CI Configuration
yaml
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm install
- run: npm run type-check
- run: npm test
- run: npm run test:coverageBest Practices
- Test Behavior, Not Implementation: Test what code does, not how
- One Assertion Per Test: Keep tests focused
- Use Descriptive Names:
it('should format currency with symbol') - Mock External Dependencies: Don't call real APIs in tests
- Test Edge Cases: Zero, null, negative, very large values
- Keep Tests Fast: Unit tests should run in milliseconds
- Maintain Test Coverage: Aim for 80%+ coverage
- Update Tests with Code: Keep tests in sync with changes
Next Steps
- Contributing Guide - Contribution guidelines
- Architecture - Project structure
- Architecture - Project structure