lightweight-browser-load-tester
Version:
A lightweight load testing tool using real browsers for streaming applications with DRM support
459 lines (372 loc) • 15.3 kB
text/typescript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { ResultsAggregator, ReportConfig } from './results-aggregator';
import {
BrowserMetrics,
DRMMetrics,
NetworkMetrics,
ErrorLog,
PrometheusConfiguration
} from '../types/index';
import fs from 'fs/promises';
import path from 'path';
// Mock fs module
vi.mock('fs/promises');
vi.mock('path');
// Mock PrometheusExporter
vi.mock('../exporters/prometheus-exporter', () => ({
PrometheusExporter: vi.fn().mockImplementation(() => ({
exportBrowserMetrics: vi.fn().mockResolvedValue(undefined),
exportDRMMetrics: vi.fn().mockResolvedValue(undefined),
exportNetworkMetrics: vi.fn().mockResolvedValue(undefined),
exportErrorMetrics: vi.fn().mockResolvedValue(undefined),
exportTestSummary: vi.fn().mockResolvedValue(undefined),
shutdown: vi.fn().mockResolvedValue(undefined)
}))
}));
describe('ResultsAggregator', () => {
let aggregator: ResultsAggregator;
let mockBrowserMetrics: BrowserMetrics;
let mockDRMMetrics: DRMMetrics;
let mockNetworkMetrics: NetworkMetrics[];
let mockErrorLog: ErrorLog;
beforeEach(() => {
aggregator = new ResultsAggregator();
mockBrowserMetrics = {
instanceId: 'browser-1',
memoryUsage: 150.5,
cpuUsage: 25.3,
requestCount: 100,
errorCount: 2,
uptime: 300
};
mockDRMMetrics = {
licenseRequestCount: 50,
averageLicenseTime: 250.5,
licenseSuccessRate: 0.96,
drmType: 'widevine',
errors: []
};
mockNetworkMetrics = [
{
url: 'https://example.com/api/test',
method: 'GET',
responseTime: 150,
statusCode: 200,
timestamp: new Date('2024-01-01T10:00:00Z'),
requestSize: 1024,
responseSize: 2048,
isStreamingRelated: true,
streamingType: 'manifest'
},
{
url: 'https://example.com/api/fail',
method: 'POST',
responseTime: 500,
statusCode: 500,
timestamp: new Date('2024-01-01T10:01:00Z'),
requestSize: 512,
responseSize: 256,
isStreamingRelated: false
}
];
mockErrorLog = {
timestamp: new Date('2024-01-01T10:00:30Z'),
level: 'error',
message: 'Test error message',
stack: 'Error stack trace',
context: { userId: 'test-user' }
};
// Reset mocks
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('constructor', () => {
it('should initialize with empty test results', () => {
const results = aggregator.getResults();
expect(results.summary.totalRequests).toBe(0);
expect(results.summary.successfulRequests).toBe(0);
expect(results.summary.failedRequests).toBe(0);
expect(results.browserMetrics).toHaveLength(0);
expect(results.drmMetrics).toHaveLength(0);
expect(results.networkMetrics).toHaveLength(0);
expect(results.errors).toHaveLength(0);
});
it('should initialize with Prometheus exporter when config provided', () => {
const prometheusConfig: PrometheusConfiguration = {
enabled: true,
remoteWriteUrl: 'https://prometheus.example.com/api/v1/write'
};
const aggregatorWithPrometheus = new ResultsAggregator(prometheusConfig);
// Should not throw and should initialize properly
expect(aggregatorWithPrometheus).toBeDefined();
});
it('should initialize without Prometheus exporter when config not provided', () => {
const aggregatorWithoutPrometheus = new ResultsAggregator();
expect(aggregatorWithoutPrometheus).toBeDefined();
});
});
describe('addBrowserMetrics', () => {
it('should add browser metrics to the results', () => {
aggregator.addBrowserMetrics(mockBrowserMetrics);
const results = aggregator.getResults();
expect(results.browserMetrics).toHaveLength(1);
expect(results.browserMetrics[0]).toEqual(mockBrowserMetrics);
});
it('should add multiple browser metrics', () => {
const secondMetrics = { ...mockBrowserMetrics, instanceId: 'browser-2' };
aggregator.addBrowserMetrics(mockBrowserMetrics);
aggregator.addBrowserMetrics(secondMetrics);
const results = aggregator.getResults();
expect(results.browserMetrics).toHaveLength(2);
expect(results.browserMetrics[0].instanceId).toBe('browser-1');
expect(results.browserMetrics[1].instanceId).toBe('browser-2');
});
});
describe('addDRMMetrics', () => {
it('should add DRM metrics to the results', () => {
aggregator.addDRMMetrics(mockDRMMetrics);
const results = aggregator.getResults();
expect(results.drmMetrics).toHaveLength(1);
expect(results.drmMetrics[0]).toEqual(mockDRMMetrics);
});
});
describe('addNetworkMetrics', () => {
it('should add network metrics to the results', () => {
aggregator.addNetworkMetrics(mockNetworkMetrics);
const results = aggregator.getResults();
expect(results.networkMetrics).toHaveLength(2);
expect(results.networkMetrics).toEqual(mockNetworkMetrics);
});
it('should handle empty network metrics array', () => {
aggregator.addNetworkMetrics([]);
const results = aggregator.getResults();
expect(results.networkMetrics).toHaveLength(0);
});
});
describe('addError', () => {
it('should add error log to the results', () => {
aggregator.addError(mockErrorLog);
const results = aggregator.getResults();
expect(results.errors).toHaveLength(1);
expect(results.errors[0]).toEqual(mockErrorLog);
});
});
describe('finalize', () => {
beforeEach(() => {
// Add test data
aggregator.addBrowserMetrics(mockBrowserMetrics);
aggregator.addNetworkMetrics(mockNetworkMetrics);
aggregator.addDRMMetrics(mockDRMMetrics);
aggregator.addError(mockErrorLog);
});
it('should calculate summary statistics correctly', () => {
// Add a small delay to ensure test duration is measurable
const startTime = Date.now();
while (Date.now() - startTime < 10) {
// Small delay
}
aggregator.finalize();
const results = aggregator.getResults();
const summary = results.summary;
expect(summary.totalRequests).toBe(2);
expect(summary.successfulRequests).toBe(1); // Only status 200
expect(summary.failedRequests).toBe(1); // Status 500
expect(summary.averageResponseTime).toBe(325); // (150 + 500) / 2
expect(summary.peakConcurrentUsers).toBe(1); // One browser instance
expect(summary.testDuration).toBeGreaterThanOrEqual(0);
});
it('should handle empty network metrics when calculating summary', () => {
const emptyAggregator = new ResultsAggregator();
emptyAggregator.finalize();
const results = emptyAggregator.getResults();
const summary = results.summary;
expect(summary.totalRequests).toBe(0);
expect(summary.successfulRequests).toBe(0);
expect(summary.failedRequests).toBe(0);
expect(summary.averageResponseTime).toBe(0);
});
});
describe('generateReports', () => {
const mockConfig: ReportConfig = {
outputPath: './test-output',
formats: ['json', 'html', 'csv'],
includeDetailedMetrics: true,
includeErrorAnalysis: true
};
beforeEach(() => {
// Setup mocks
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(path.join).mockImplementation((...args) => args.join('/'));
// Add test data
aggregator.addBrowserMetrics(mockBrowserMetrics);
aggregator.addNetworkMetrics(mockNetworkMetrics);
aggregator.addDRMMetrics(mockDRMMetrics);
aggregator.addError(mockErrorLog);
});
it('should generate JSON report', async () => {
const config: ReportConfig = {
...mockConfig,
formats: ['json']
};
await aggregator.generateReports(config);
expect(fs.mkdir).toHaveBeenCalledWith('./test-output', { recursive: true });
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining('load-test-results-'),
expect.stringContaining('"totalRequests"')
);
});
it('should generate HTML report', async () => {
const config: ReportConfig = {
...mockConfig,
formats: ['html']
};
await aggregator.generateReports(config);
expect(fs.mkdir).toHaveBeenCalledWith('./test-output', { recursive: true });
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining('load-test-results-'),
expect.stringContaining('<!DOCTYPE html>')
);
});
it('should generate CSV reports', async () => {
const config: ReportConfig = {
...mockConfig,
formats: ['csv']
};
await aggregator.generateReports(config);
expect(fs.mkdir).toHaveBeenCalledWith('./test-output', { recursive: true });
expect(fs.writeFile).toHaveBeenCalledTimes(3); // summary, network, browser CSVs
// Check that CSV files are created
const writeFileCalls = vi.mocked(fs.writeFile).mock.calls;
expect(writeFileCalls.some(call => call[0].includes('summary'))).toBe(true);
expect(writeFileCalls.some(call => call[0].includes('network'))).toBe(true);
expect(writeFileCalls.some(call => call[0].includes('browser'))).toBe(true);
});
it('should generate multiple report formats', async () => {
await aggregator.generateReports(mockConfig);
expect(fs.writeFile).toHaveBeenCalledTimes(5); // 1 JSON + 1 HTML + 3 CSV files
});
it('should use default output path when not specified', async () => {
const config: ReportConfig = {
formats: ['json']
};
await aggregator.generateReports(config);
expect(fs.mkdir).toHaveBeenCalledWith('./test-results', { recursive: true });
});
it('should finalize results if not already done', async () => {
const config: ReportConfig = {
formats: ['json']
};
// Don't call finalize manually
await aggregator.generateReports(config);
const results = aggregator.getResults();
expect(results.summary.totalRequests).toBe(2); // Should be calculated
});
});
describe('HTML report generation', () => {
beforeEach(() => {
aggregator.addBrowserMetrics(mockBrowserMetrics);
aggregator.addNetworkMetrics(mockNetworkMetrics);
aggregator.addDRMMetrics(mockDRMMetrics);
aggregator.addError(mockErrorLog);
aggregator.finalize();
});
it('should include summary metrics in HTML', async () => {
const config: ReportConfig = {
formats: ['html'],
includeDetailedMetrics: false,
includeErrorAnalysis: false
};
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(path.join).mockImplementation((...args) => args.join('/'));
await aggregator.generateReports(config);
const htmlContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
expect(htmlContent).toContain('Total Requests');
expect(htmlContent).toContain('2'); // Total requests
expect(htmlContent).toContain('50.0%'); // Success rate
});
it('should include detailed metrics when requested', async () => {
const config: ReportConfig = {
formats: ['html'],
includeDetailedMetrics: true,
includeErrorAnalysis: false
};
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(path.join).mockImplementation((...args) => args.join('/'));
await aggregator.generateReports(config);
const htmlContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
expect(htmlContent).toContain('Browser Instance Metrics');
expect(htmlContent).toContain('browser-1');
});
it('should include error analysis when requested', async () => {
const config: ReportConfig = {
formats: ['html'],
includeDetailedMetrics: false,
includeErrorAnalysis: true
};
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(path.join).mockImplementation((...args) => args.join('/'));
await aggregator.generateReports(config);
const htmlContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
expect(htmlContent).toContain('Error Analysis');
expect(htmlContent).toContain('Test error message');
});
});
describe('CSV report generation', () => {
beforeEach(() => {
aggregator.addBrowserMetrics(mockBrowserMetrics);
aggregator.addNetworkMetrics(mockNetworkMetrics);
aggregator.finalize();
});
it('should generate valid CSV format for summary', async () => {
const config: ReportConfig = {
formats: ['csv']
};
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(path.join).mockImplementation((...args) => args.join('/'));
await aggregator.generateReports(config);
const summaryCSV = vi.mocked(fs.writeFile).mock.calls
.find(call => call[0].includes('summary'))?.[1] as string;
expect(summaryCSV).toContain('Total Requests,Successful Requests');
expect(summaryCSV).toContain('2,1,1'); // totalRequests, successfulRequests, failedRequests
});
it('should generate valid CSV format for network metrics', async () => {
const config: ReportConfig = {
formats: ['csv']
};
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(path.join).mockImplementation((...args) => args.join('/'));
await aggregator.generateReports(config);
const networkCSV = vi.mocked(fs.writeFile).mock.calls
.find(call => call[0].includes('network'))?.[1] as string;
expect(networkCSV).toContain('URL,Method,Response Time');
expect(networkCSV).toContain('https://example.com/api/test');
expect(networkCSV).toContain('GET,150,200');
});
});
describe('Prometheus integration', () => {
it('should not export metrics when Prometheus is not configured', () => {
const aggregatorWithoutPrometheus = new ResultsAggregator();
expect(() => {
aggregatorWithoutPrometheus.addBrowserMetrics(mockBrowserMetrics);
aggregatorWithoutPrometheus.addDRMMetrics(mockDRMMetrics);
aggregatorWithoutPrometheus.addNetworkMetrics(mockNetworkMetrics);
aggregatorWithoutPrometheus.addError(mockErrorLog);
aggregatorWithoutPrometheus.finalize();
}).not.toThrow();
});
});
describe('shutdown', () => {
it('should shutdown without Prometheus exporter', async () => {
const aggregatorWithoutPrometheus = new ResultsAggregator();
await expect(aggregatorWithoutPrometheus.shutdown()).resolves.not.toThrow();
});
});
});