Test Package (@schwab/test)
Overview
The @schwab/test package provides comprehensive testing utilities, configurations, and frameworks for the Charles Schwab monorepo. It includes Jest configurations, mock utilities, test helpers, and automated testing infrastructure that ensures consistent testing practices across all applications and packages.
Architecture
Core Testing Configurations
1. Jest Base Configuration
Jest Base Configuration
// jest.base.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@schwab/test/setup'],
moduleNameMapping: {
'^@schwab/(.*)$': '<rootDir>/packages/$1/src',
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\\.(ts|tsx)$': ['ts-jest', {
tsconfig: {
jsx: 'react-jsx'
}
}],
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.{ts,tsx}',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
2. Mock Utilities
Comprehensive Mock System
// src/mock/base/Mockery.ts
export class Mockery {
/**
* Mocks the evaluateStaticFlag response
*/
static mockEvaluateStaticFlag(flags: Record<string, boolean> | undefined): void {
jest.doMock('@schwab/utilities/flags', () => ({
evaluateStaticFlag: () => Promise.resolve(flags),
}));
}
/**
* Mocks the evaluateDynamicFlag response
*/
static mockEvaluateDynamicFlag(flags: Record<string, boolean> | undefined): void {
jest.doMock('@schwab/utilities/flags', () => ({
evaluateDynamicFlag: () => Promise.resolve(flags),
}));
}
/**
* Mock Next.js router
*/
static mockNextRouter(overrides = {}) {
const mockRouter = {
push: jest.fn(),
replace: jest.fn(),
back: jest.fn(),
pathname: '/',
query: {},
asPath: '/',
...overrides,
};
jest.doMock('next/router', () => ({
useRouter: () => mockRouter,
}));
return mockRouter;
}
/**
* Mock API responses
*/
static mockApiResponse<T>(data: T, isOk: boolean = true) {
return {
isOk,
data: isOk ? data : null,
error: isOk ? null : 'Mock API Error',
cacheTags: ['mock-tag'],
};
}
}
Test Helper Utilities
1. Component Testing Helpers
React Component Test Utilities
// src/helpers/componentTestHelpers.ts
import { render, RenderOptions } from '@testing-library/react';
import { ReactElement } from 'react';
interface CustomRenderOptions extends RenderOptions {
withProviders?: boolean;
routerProps?: Record<string, any>;
}
export function renderWithProviders(
ui: ReactElement,
options: CustomRenderOptions = {}
) {
const { withProviders = true, routerProps = {}, ...renderOptions } = options;
function Wrapper({ children }: { children: React.ReactNode }) {
if (!withProviders) {
return <>{children}</>;
}
return (
<MockProviders routerProps={routerProps}>
{children}
</MockProviders>
);
}
return {
...render(ui, { wrapper: Wrapper, ...renderOptions }),
};
}
export function MockProviders({
children,
routerProps = {}
}: {
children: React.ReactNode;
routerProps?: Record<string, any>;
}) {
const mockRouter = {
pathname: '/',
query: {},
asPath: '/',
push: jest.fn(),
replace: jest.fn(),
...routerProps,
};
return (
<RouterContext.Provider value={mockRouter}>
<ThemeProvider>
{children}
</ThemeProvider>
</RouterContext.Provider>
);
}
2. API Testing Utilities
API Test Helpers
// src/helpers/apiTestHelpers.ts
export class ApiTestHelper {
/**
* Create mock fetch response
*/
static createMockResponse<T>(
data: T,
status: number = 200,
headers: Record<string, string> = {}
): Promise<Response> {
return Promise.resolve({
ok: status >= 200 && status < 300,
status,
headers: new Headers(headers),
json: () => Promise.resolve(data),
text: () => Promise.resolve(JSON.stringify(data)),
} as Response);
}
/**
* Mock global fetch
*/
static mockGlobalFetch(responses: Record<string, any>) {
const mockFetch = jest.fn().mockImplementation((url: string) => {
const response = responses[url];
if (response) {
return this.createMockResponse(response.data, response.status);
}
return this.createMockResponse(null, 404);
});
global.fetch = mockFetch;
return mockFetch;
}
/**
* Restore global fetch
*/
static restoreGlobalFetch() {
global.fetch = originalFetch;
}
}
3. Form Testing Utilities
Form Test Helpers
// src/helpers/formTestHelpers.ts
import { fireEvent, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
export class FormTestHelper {
/**
* Fill form fields by label
*/
static async fillFormField(labelText: string, value: string) {
const user = userEvent.setup();
const field = screen.getByLabelText(labelText);
await user.clear(field);
await user.type(field, value);
}
/**
* Submit form and wait for completion
*/
static async submitForm(formTestId?: string) {
const form = formTestId
? screen.getByTestId(formTestId)
: screen.getByRole('form');
fireEvent.submit(form);
await waitFor(() => {
// Wait for form submission to complete
});
}
/**
* Fill complete form data
*/
static async fillForm(formData: Record<string, string>) {
const user = userEvent.setup();
for (const [label, value] of Object.entries(formData)) {
await this.fillFormField(label, value);
}
}
/**
* Test form validation
*/
static async testFormValidation(
invalidData: Record<string, string>,
expectedErrors: string[]
) {
await this.fillForm(invalidData);
await this.submitForm();
for (const error of expectedErrors) {
expect(screen.getByText(error)).toBeInTheDocument();
}
}
}
Custom Jest Matchers
1. Component-Specific Matchers
Custom Jest Matchers
// src/matchers/componentMatchers.ts
expect.extend({
toHaveAriaLabel(received, expected) {
const pass = received.getAttribute('aria-label') === expected;
if (pass) {
return {
message: () => `Expected element not to have aria-label "${expected}"`,
pass: true,
};
} else {
return {
message: () => `Expected element to have aria-label "${expected}"`,
pass: false,
};
}
},
toHaveValidSEO(received) {
const title = received.querySelector('title');
const metaDescription = received.querySelector('meta[name="description"]');
const pass = title && metaDescription && title.textContent.length > 0;
if (pass) {
return {
message: () => 'Expected element not to have valid SEO',
pass: true,
};
} else {
return {
message: () => 'Expected element to have valid SEO (title and meta description)',
pass: false,
};
}
},
toBeAccessible(received) {
// Simplified accessibility check
const hasAriaLabel = received.getAttribute('aria-label');
const hasRole = received.getAttribute('role');
const hasTabIndex = received.getAttribute('tabindex') !== '-1';
const pass = hasAriaLabel || hasRole || hasTabIndex;
return {
message: () => pass
? 'Expected element not to be accessible'
: 'Expected element to be accessible (needs aria-label, role, or proper tabindex)',
pass,
};
},
});
2. API Response Matchers
API Response Matchers
expect.extend({
toBeValidApiResponse(received) {
const hasIsOk = typeof received.isOk === 'boolean';
const hasData = 'data' in received;
const hasError = 'error' in received;
const pass = hasIsOk && hasData && hasError;
return {
message: () => pass
? 'Expected not to be a valid API response'
: 'Expected to be a valid API response with isOk, data, and error properties',
pass,
};
},
toHaveSuccessfulApiResponse(received) {
const pass = received.isOk === true && received.data !== null;
return {
message: () => pass
? 'Expected API response not to be successful'
: 'Expected API response to be successful (isOk: true, data not null)',
pass,
};
},
});
Integration Test Patterns
1. Database Integration Tests
Database Integration Testing
// src/integration/databaseTests.ts
export class DatabaseTestHelper {
/**
* Setup test database
*/
static async setupTestDatabase() {
// Initialize test database
await initializeTestDB();
}
/**
* Cleanup test data
*/
static async cleanupTestData() {
// Clean up test data after each test
await cleanupDB();
}
/**
* Seed test data
*/
static async seedTestData(fixtures: Record<string, any[]>) {
for (const [table, data] of Object.entries(fixtures)) {
await insertTestData(table, data);
}
}
/**
* Test database operations
*/
static async testDatabaseOperation<T>(
operation: () => Promise<T>,
expectedResult: T
) {
const result = await operation();
expect(result).toEqual(expectedResult);
}
}
2. E2E Test Utilities
End-to-End Test Helpers
// src/e2e/e2eTestHelpers.ts
export class E2ETestHelper {
/**
* Page object model base
*/
static createPageObject(page: any) {
return {
goto: (url: string) => page.goto(url),
fillForm: async (formData: Record<string, string>) => {
for (const [selector, value] of Object.entries(formData)) {
await page.fill(`[name="${selector}"]`, value);
}
},
submitForm: async (formSelector: string = 'form') => {
await page.click(`${formSelector} [type="submit"]`);
},
waitForNavigation: () => page.waitForNavigation(),
takeScreenshot: (name: string) =>
page.screenshot({ path: `screenshots/${name}.png` }),
};
}
/**
* Test user flows
*/
static async testUserFlow(
page: any,
steps: Array<{
action: string;
selector?: string;
value?: string;
assertion?: string;
}>
) {
for (const step of steps) {
switch (step.action) {
case 'click':
await page.click(step.selector);
break;
case 'fill':
await page.fill(step.selector, step.value);
break;
case 'navigate':
await page.goto(step.value);
break;
case 'assert':
await expect(page.locator(step.selector)).toBeVisible();
break;
}
}
}
}
Performance Testing
1. Performance Test Utilities
Performance Testing
// src/performance/performanceTests.ts
export class PerformanceTestHelper {
/**
* Measure function execution time
*/
static async measureExecutionTime<T>(
operation: () => Promise<T>,
maxTimeMs: number = 1000
): Promise<{ result: T; executionTime: number }> {
const startTime = performance.now();
const result = await operation();
const executionTime = performance.now() - startTime;
expect(executionTime).toBeLessThan(maxTimeMs);
return { result, executionTime };
}
/**
* Memory usage testing
*/
static measureMemoryUsage<T>(operation: () => T): T {
const initialMemory = process.memoryUsage();
const result = operation();
const finalMemory = process.memoryUsage();
const memoryDiff = {
rss: finalMemory.rss - initialMemory.rss,
heapUsed: finalMemory.heapUsed - initialMemory.heapUsed,
};
console.log('Memory usage difference:', memoryDiff);
return result;
}
/**
* Load testing simulation
*/
static async simulateLoad(
operation: () => Promise<any>,
concurrentRequests: number = 10,
iterations: number = 100
) {
const results = [];
for (let i = 0; i < iterations; i++) {
const promises = Array(concurrentRequests)
.fill(null)
.map(() => this.measureExecutionTime(operation));
const batchResults = await Promise.all(promises);
results.push(...batchResults);
}
const averageTime = results.reduce((sum, r) => sum + r.executionTime, 0) / results.length;
const maxTime = Math.max(...results.map(r => r.executionTime));
return { averageTime, maxTime, totalRequests: results.length };
}
}
Test Data Factories
1. Mock Data Factories
Test Data Factories
// src/factories/testDataFactories.ts
export class TestDataFactory {
/**
* Create test user data
*/
static createUser(overrides: Partial<User> = {}): User {
return {
id: '123',
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
phone: '(555) 123-4567',
...overrides,
};
}
/**
* Create test component props
*/
static createComponentProps<T>(
baseProps: T,
overrides: Partial<T> = {}
): T {
return {
...baseProps,
...overrides,
};
}
/**
* Create test API response
*/
static createApiResponse<T>(
data: T,
isOk: boolean = true,
error: string | null = null
) {
return {
isOk,
data: isOk ? data : null,
error: isOk ? null : error,
cacheTags: ['test-tag'],
};
}
/**
* Create test form data
*/
static createFormData(data: Record<string, string | string[]>): FormData {
const formData = new FormData();
Object.entries(data).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => formData.append(key, v));
} else {
formData.append(key, value);
}
});
return formData;
}
}
Test Coverage and Reporting
1. Coverage Configuration
Coverage Configuration
// jest.coverage.config.js
module.exports = {
...baseConfig,
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html', 'json'],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.{ts,tsx}',
'!src/**/__tests__/**',
'!src/**/__mocks__/**',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
'src/components/': {
branches: 85,
functions: 85,
lines: 85,
statements: 85,
},
},
};
Continuous Integration
1. CI Test Configuration
GitHub Actions Test Workflow
# .github/workflows/test.yml
name: Test Suite
on:
pull_request:
push:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run linter
run: pnpm lint
- name: Run type checking
run: pnpm type-check
- name: Run unit tests
run: pnpm test --coverage
- name: Run integration tests
run: pnpm test:integration
- name: Upload coverage reports
uses: codecov/codecov-action@v3
The Test package provides comprehensive testing infrastructure that ensures code quality, reliability, and maintainability across the Charles Schwab monorepo through standardized testing practices, utilities, and automation.