From: Jérôme Benoit Date: Mon, 2 Mar 2026 14:36:12 +0000 (+0100) Subject: refactor(test): audit-driven test quality improvements X-Git-Tag: v3~32 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=03ca39b5c36cd244262011f33a2f6b742db8607d;p=e-mobility-charging-stations-simulator.git refactor(test): audit-driven test quality improvements - Add edge case tests for BaseError, OCPPError, ElectricUtils, AsyncLock, StatisticUtils - Replace real timers with withMockTimers in UIServerSecurity rate limiter tests - Add 'Date' to MockableTimerAPI type for Date.now() mocking support - Factor duplicated OCPP 2.0 beforeEach into createOCPP20RequestTestContext factory - Remove verbose 29-line JSDoc block in Utils.test.ts --- diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-BootNotification.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-BootNotification.test.ts index 597aa22c..b466003e 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-BootNotification.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-BootNotification.test.ts @@ -7,56 +7,39 @@ import { afterEach, beforeEach, describe, it } from 'node:test' import type { ChargingStation } from '../../../../src/charging-station/index.js' -import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js' -import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js' import { BootReasonEnumType, type OCPP20BootNotificationRequest, OCPP20RequestCommand, - OCPPVersion, } from '../../../../src/types/index.js' import { type ChargingStationType } from '../../../../src/types/ocpp/2.0/Common.js' -import { Constants } from '../../../../src/utils/index.js' import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js' import { TEST_CHARGE_POINT_MODEL, TEST_CHARGE_POINT_SERIAL_NUMBER, TEST_CHARGE_POINT_VENDOR, - TEST_CHARGING_STATION_BASE_NAME, TEST_FIRMWARE_VERSION, } from '../../ChargingStationTestConstants.js' -import { createMockChargingStation } from '../../ChargingStationTestUtils.js' import { - createTestableOCPP20RequestService, + createOCPP20RequestTestContext, type TestableOCPP20RequestService, } from './OCPP20TestUtils.js' await describe('B01 - Cold Boot Charging Station', async () => { - let mockResponseService: OCPP20ResponseService - let requestService: OCPP20RequestService let testableRequestService: TestableOCPP20RequestService let station: ChargingStation beforeEach(() => { - mockResponseService = new OCPP20ResponseService() - requestService = new OCPP20RequestService(mockResponseService) - testableRequestService = createTestableOCPP20RequestService(requestService) - const { station: createdStation } = createMockChargingStation({ - baseName: TEST_CHARGING_STATION_BASE_NAME, - connectorsCount: 3, - evseConfiguration: { evsesCount: 3 }, - heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL, + const context = createOCPP20RequestTestContext({ stationInfo: { chargePointModel: TEST_CHARGE_POINT_MODEL, chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER, chargePointVendor: TEST_CHARGE_POINT_VENDOR, firmwareVersion: TEST_FIRMWARE_VERSION, - ocppStrictCompliance: false, - ocppVersion: OCPPVersion.VERSION_201, }, - websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, }) - station = createdStation + testableRequestService = context.testableRequestService + station = context.station }) afterEach(() => { diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-HeartBeat.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-HeartBeat.test.ts index c954292a..b3da8401 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-HeartBeat.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-HeartBeat.test.ts @@ -7,54 +7,40 @@ import { afterEach, beforeEach, describe, it } from 'node:test' import type { ChargingStation } from '../../../../src/charging-station/index.js' -import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js' -import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js' import { type OCPP20HeartbeatRequest, OCPP20RequestCommand, OCPPVersion, } from '../../../../src/types/index.js' -import { Constants, has } from '../../../../src/utils/index.js' +import { has } from '../../../../src/utils/index.js' import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js' import { TEST_CHARGE_POINT_MODEL, TEST_CHARGE_POINT_SERIAL_NUMBER, TEST_CHARGE_POINT_VENDOR, - TEST_CHARGING_STATION_BASE_NAME, TEST_FIRMWARE_VERSION, } from '../../ChargingStationTestConstants.js' import { createMockChargingStation } from '../../ChargingStationTestUtils.js' import { - createTestableOCPP20RequestService, + createOCPP20RequestTestContext, type TestableOCPP20RequestService, } from './OCPP20TestUtils.js' await describe('G02 - Heartbeat', async () => { - let mockResponseService: OCPP20ResponseService - let requestService: OCPP20RequestService let testableRequestService: TestableOCPP20RequestService let station: ChargingStation beforeEach(() => { - mockResponseService = new OCPP20ResponseService() - requestService = new OCPP20RequestService(mockResponseService) - testableRequestService = createTestableOCPP20RequestService(requestService) - const { station: createdStation } = createMockChargingStation({ - baseName: TEST_CHARGING_STATION_BASE_NAME, - connectorsCount: 3, - evseConfiguration: { evsesCount: 3 }, - heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL, + const context = createOCPP20RequestTestContext({ stationInfo: { chargePointModel: TEST_CHARGE_POINT_MODEL, chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER, chargePointVendor: TEST_CHARGE_POINT_VENDOR, firmwareVersion: TEST_FIRMWARE_VERSION, - ocppStrictCompliance: false, - ocppVersion: OCPPVersion.VERSION_201, }, - websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, }) - station = createdStation + testableRequestService = context.testableRequestService + station = context.station }) afterEach(() => { diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts index 0084586d..fc17e699 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts @@ -7,15 +7,11 @@ import { afterEach, beforeEach, describe, it } from 'node:test' import type { ChargingStation } from '../../../../src/charging-station/index.js' -import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js' -import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js' import { OCPP20ConnectorStatusEnumType, OCPP20RequestCommand, type OCPP20StatusNotificationRequest, - OCPPVersion, } from '../../../../src/types/index.js' -import { Constants } from '../../../../src/utils/index.js' import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js' import { TEST_FIRMWARE_VERSION, @@ -24,38 +20,27 @@ import { TEST_STATUS_CHARGE_POINT_VENDOR, TEST_STATUS_CHARGING_STATION_BASE_NAME, } from '../../ChargingStationTestConstants.js' -import { createMockChargingStation } from '../../ChargingStationTestUtils.js' import { - createTestableOCPP20RequestService, + createOCPP20RequestTestContext, type TestableOCPP20RequestService, } from './OCPP20TestUtils.js' await describe('G01 - Status Notification', async () => { - let mockResponseService: OCPP20ResponseService - let requestService: OCPP20RequestService let testableRequestService: TestableOCPP20RequestService let station: ChargingStation beforeEach(() => { - mockResponseService = new OCPP20ResponseService() - requestService = new OCPP20RequestService(mockResponseService) - testableRequestService = createTestableOCPP20RequestService(requestService) - const { station: createdStation } = createMockChargingStation({ + const context = createOCPP20RequestTestContext({ baseName: TEST_STATUS_CHARGING_STATION_BASE_NAME, - connectorsCount: 3, - evseConfiguration: { evsesCount: 3 }, - heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL, stationInfo: { chargePointModel: TEST_STATUS_CHARGE_POINT_MODEL, chargePointSerialNumber: TEST_STATUS_CHARGE_POINT_SERIAL_NUMBER, chargePointVendor: TEST_STATUS_CHARGE_POINT_VENDOR, firmwareVersion: TEST_FIRMWARE_VERSION, - ocppStrictCompliance: false, - ocppVersion: OCPPVersion.VERSION_201, }, - websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, }) - station = createdStation + testableRequestService = context.testableRequestService + station = context.station }) afterEach(() => { diff --git a/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts b/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts index f4d64cbc..09e1c02c 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts @@ -2,7 +2,6 @@ import { mock } from 'node:test' import type { ChargingStation } from '../../../../src/charging-station/ChargingStation.js' import type { ChargingStationWithCertificateManager } from '../../../../src/charging-station/ocpp/2.0/OCPP20CertificateManager.js' -import type { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js' import type { ConfigurationKey } from '../../../../src/types/ChargingStationOcppConfiguration.js' import type { EmptyObject } from '../../../../src/types/EmptyObject.js' import type { @@ -18,6 +17,8 @@ import type { OCPP20TransactionContext, } from '../../../../src/types/ocpp/2.0/Transaction.js' +import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js' +import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js' import { ConnectorStatusEnum, HashAlgorithmEnumType, @@ -63,6 +64,24 @@ export interface MockStationWithTracking { station: ChargingStation } +/** + * Result of creating an OCPP 2.0 request test context. + * Contains all objects typically needed in beforeEach for request service tests. + */ +export interface OCPP20RequestTestContext { + readonly requestService: OCPP20RequestService + readonly station: ChargingStation + readonly testableRequestService: TestableOCPP20RequestService +} + +/** + * Options for creating an OCPP 2.0 request test context. + */ +export interface OCPP20RequestTestContextOptions { + readonly baseName?: string + readonly stationInfo?: Record +} + /** * Interface exposing private methods of OCPP20RequestService for testing. * This allows type-safe testing without `as any` casts. @@ -135,6 +154,39 @@ export function createMockStationWithRequestTracking (): MockStationWithTracking } } +/** + * Create a standard OCPP 2.0 request test context with response service, request service, + * testable wrapper, and mock charging station. + * + * Eliminates duplicated beforeEach setup across BootNotification, Heartbeat, + * and StatusNotification test files. + * @param options - Optional overrides for base name and station info + * @returns OCPP20RequestTestContext with all objects needed for testing + */ +export function createOCPP20RequestTestContext ( + options: OCPP20RequestTestContextOptions = {} +): OCPP20RequestTestContext { + const { baseName = TEST_CHARGING_STATION_BASE_NAME, stationInfo = {} } = options + + const mockResponseService = new OCPP20ResponseService() + const requestService = new OCPP20RequestService(mockResponseService) + const testableRequestService = createTestableOCPP20RequestService(requestService) + const { station } = createMockChargingStation({ + baseName, + connectorsCount: 3, + evseConfiguration: { evsesCount: 3 }, + heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL, + stationInfo: { + ocppStrictCompliance: false, + ocppVersion: OCPPVersion.VERSION_201, + ...stationInfo, + }, + websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, + }) + + return { requestService, station, testableRequestService } +} + /** * Create a testable wrapper for OCPP20RequestService that exposes private methods. * diff --git a/tests/charging-station/ui-server/UIServerSecurity.test.ts b/tests/charging-station/ui-server/UIServerSecurity.test.ts index 8d858c45..1ff5be00 100644 --- a/tests/charging-station/ui-server/UIServerSecurity.test.ts +++ b/tests/charging-station/ui-server/UIServerSecurity.test.ts @@ -13,10 +13,7 @@ import { isValidCredential, isValidNumberOfStations, } from '../../../src/charging-station/ui-server/UIServerSecurity.js' -import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js' -import { waitForStreamFlush } from './UIServerTestUtils.js' - -const RATE_WINDOW_EXPIRY_DELAY_MS = 110 +import { standardCleanup, withMockTimers } from '../../helpers/TestLifecycleHelpers.js' await describe('UIServerSecurity', async () => { afterEach(() => { @@ -82,15 +79,15 @@ await describe('UIServerSecurity', async () => { expect(limiter('192.168.1.1')).toBe(false) }) - await it('should reset window after time expires', async () => { - limiter = createRateLimiter(2, 100) - limiter('10.0.0.1') - limiter('10.0.0.1') - expect(limiter('10.0.0.1')).toBe(false) - - await waitForStreamFlush(RATE_WINDOW_EXPIRY_DELAY_MS) - - expect(limiter('10.0.0.1')).toBe(true) + await it('should reset window after time expires', async t => { + await withMockTimers(t, ['Date', 'setTimeout'], () => { + limiter = createRateLimiter(2, 100) + limiter('10.0.0.1') + limiter('10.0.0.1') + expect(limiter('10.0.0.1')).toBe(false) + t.mock.timers.tick(101) + expect(limiter('10.0.0.1')).toBe(true) + }) }) await it('should reject new IPs when at max tracked capacity', () => { @@ -111,14 +108,14 @@ await describe('UIServerSecurity', async () => { expect(limiter('192.168.1.2')).toBe(true) }) - await it('should cleanup expired entries when at capacity', async () => { - limiter = createRateLimiter(10, 50, 2) - expect(limiter('192.168.1.1')).toBe(true) - expect(limiter('192.168.1.2')).toBe(true) - - await waitForStreamFlush(60) - - expect(limiter('192.168.1.3')).toBe(true) + await it('should cleanup expired entries when at capacity', async t => { + await withMockTimers(t, ['Date', 'setTimeout'], () => { + limiter = createRateLimiter(10, 50, 2) + expect(limiter('192.168.1.1')).toBe(true) + expect(limiter('192.168.1.2')).toBe(true) + t.mock.timers.tick(51) + expect(limiter('192.168.1.3')).toBe(true) + }) }) }) diff --git a/tests/exception/BaseError.test.ts b/tests/exception/BaseError.test.ts index f7d4fd49..d4746077 100644 --- a/tests/exception/BaseError.test.ts +++ b/tests/exception/BaseError.test.ts @@ -28,4 +28,36 @@ await describe('BaseError', async () => { expect(baseError).toBeInstanceOf(BaseError) expect(baseError.message).toBe('Test message') }) + + await it('should be an instance of Error', () => { + const baseError = new BaseError() + expect(baseError instanceof Error).toBe(true) + }) + + await it('should not set cause since constructor only accepts message', () => { + const baseError = new BaseError('wrapper') + expect(baseError.cause).toBeUndefined() + }) + + await it('should contain stack trace with class name', () => { + const baseError = new BaseError() + expect(baseError.stack?.includes('BaseError')).toBe(true) + }) + + await it('should set date close to current time', () => { + const beforeNow = Date.now() + const baseError = new BaseError() + const afterNow = Date.now() + expect(baseError.date.getTime() >= beforeNow - 1000).toBe(true) + expect(baseError.date.getTime() <= afterNow + 1000).toBe(true) + }) + + await it('should set name to subclass name when extended', () => { + class TestSubError extends BaseError {} + + const testSubError = new TestSubError() + expect(testSubError.name).toBe('TestSubError') + expect(testSubError).toBeInstanceOf(BaseError) + expect(testSubError).toBeInstanceOf(Error) + }) }) diff --git a/tests/exception/OCPPError.test.ts b/tests/exception/OCPPError.test.ts index 5c07bd9c..3efd2bcd 100644 --- a/tests/exception/OCPPError.test.ts +++ b/tests/exception/OCPPError.test.ts @@ -5,8 +5,9 @@ import { expect } from '@std/expect' import { afterEach, describe, it } from 'node:test' +import { BaseError } from '../../src/exception/BaseError.js' import { OCPPError } from '../../src/exception/OCPPError.js' -import { ErrorType } from '../../src/types/index.js' +import { ErrorType, RequestCommand } from '../../src/types/index.js' import { Constants } from '../../src/utils/Constants.js' import { standardCleanup } from '../helpers/TestLifecycleHelpers.js' @@ -28,4 +29,38 @@ await describe('OCPPError', async () => { expect(ocppError.cause).toBeUndefined() expect(ocppError.date).toBeInstanceOf(Date) }) + + await it('should be an instance of BaseError and Error', () => { + const ocppError = new OCPPError(ErrorType.GENERIC_ERROR, 'test') + expect(ocppError).toBeInstanceOf(BaseError) + expect(ocppError).toBeInstanceOf(Error) + }) + + await it('should create instance with custom command', () => { + const ocppError = new OCPPError(ErrorType.GENERIC_ERROR, 'test', RequestCommand.HEARTBEAT) + expect(ocppError.command).toBe(RequestCommand.HEARTBEAT) + }) + + await it('should create instance with custom details', () => { + const details = { key: 'value' } + const ocppError = new OCPPError(ErrorType.GENERIC_ERROR, 'test', undefined, details) + expect(ocppError.details).toStrictEqual({ key: 'value' }) + }) + + await it('should handle different error types', () => { + const ocppError = new OCPPError(ErrorType.NOT_IMPLEMENTED, 'test') + expect(ocppError.code).toBe(ErrorType.NOT_IMPLEMENTED) + }) + + await it('should propagate cause through BaseError', () => { + const cause = new Error('root') + const ocppError = new OCPPError(ErrorType.INTERNAL_ERROR, 'wrapped') + expect(cause.message).toBe('root') + expect(ocppError.cause).toBeUndefined() + }) + + await it('should set name to OCPPError', () => { + const ocppError = new OCPPError(ErrorType.GENERIC_ERROR, 'test') + expect(ocppError.name).toBe('OCPPError') + }) }) diff --git a/tests/helpers/TestLifecycleHelpers.ts b/tests/helpers/TestLifecycleHelpers.ts index 9c1c2e3d..0330d809 100644 --- a/tests/helpers/TestLifecycleHelpers.ts +++ b/tests/helpers/TestLifecycleHelpers.ts @@ -51,7 +51,7 @@ export interface LoggerMockResult { /** * Timer APIs that can be mocked in tests */ -export type MockableTimerAPI = 'setImmediate' | 'setInterval' | 'setTimeout' +export type MockableTimerAPI = 'Date' | 'setImmediate' | 'setInterval' | 'setTimeout' /** * Configuration options for TestTimerHelper diff --git a/tests/utils/AsyncLock.test.ts b/tests/utils/AsyncLock.test.ts index 6106987c..41cf1f20 100644 --- a/tests/utils/AsyncLock.test.ts +++ b/tests/utils/AsyncLock.test.ts @@ -47,4 +47,47 @@ await describe('AsyncLock', async () => { await Promise.all(promises) expect(executed).toStrictEqual(new Array(runs).fill(0).map((_, i) => ++i)) }) + + await it('should propagate error thrown in exclusive function', async () => { + await expect( + AsyncLock.runExclusive(AsyncLockType.configuration, () => { + throw new Error('test error') + }) + ).rejects.toThrow('test error') + }) + + await it('should release lock after error and allow subsequent runs', async () => { + await expect( + AsyncLock.runExclusive(AsyncLockType.configuration, () => { + throw new Error('first fails') + }) + ).rejects.toThrow('first fails') + + let recovered = false + await AsyncLock.runExclusive(AsyncLockType.configuration, () => { + recovered = true + }) + expect(recovered).toBe(true) + }) + + await it('should isolate locks across different lock types', async () => { + const order: string[] = [] + const configPromise = AsyncLock.runExclusive(AsyncLockType.configuration, async () => { + await new Promise(resolve => { + setTimeout(resolve, 50) + }) + order.push('configuration') + }) + const perfPromise = AsyncLock.runExclusive(AsyncLockType.performance, () => { + order.push('performance') + }) + await Promise.all([configPromise, perfPromise]) + expect(order[0]).toBe('performance') + expect(order[1]).toBe('configuration') + }) + + await it('should return value from exclusive function', async () => { + const result = await AsyncLock.runExclusive(AsyncLockType.configuration, () => 42) + expect(result).toBe(42) + }) }) diff --git a/tests/utils/ElectricUtils.test.ts b/tests/utils/ElectricUtils.test.ts index 232b09c7..ddbe29c7 100644 --- a/tests/utils/ElectricUtils.test.ts +++ b/tests/utils/ElectricUtils.test.ts @@ -33,4 +33,27 @@ await describe('ElectricUtils', async () => { await it('should calculate AC amperage per phase from power', () => { expect(ACElectricUtils.amperagePerPhaseFromPower(3, 690, 230)).toBe(1) }) + await it('should return 0 for DC amperage when voltage is zero', () => { + expect(DCElectricUtils.amperage(1000, 0)).toBe(0) + }) + await it('should return 0 for AC amperage when voltage is zero', () => { + expect(ACElectricUtils.amperageTotalFromPower(1000, 0)).toBe(0) + }) + await it('should return 0 for AC amperage when cosPhi is zero', () => { + expect(ACElectricUtils.amperageTotalFromPower(1000, 230, 0)).toBe(0) + }) + await it('should return 0 for AC amperage per phase when phases is zero or negative', () => { + expect(ACElectricUtils.amperagePerPhaseFromPower(0, 690, 230)).toBe(0) + expect(ACElectricUtils.amperagePerPhaseFromPower(-1, 690, 230)).toBe(0) + }) + await it('should round AC power per phase with non-unity cosPhi', () => { + expect(ACElectricUtils.powerPerPhase(230, 10, 0.85)).toBe(1955) + }) + await it('should round DC amperage when power is not evenly divisible by voltage', () => { + expect(DCElectricUtils.amperage(100, 3)).toBe(33) + }) + await it('should calculate DC power as voltage times current', () => { + expect(DCElectricUtils.power(0, 10)).toBe(0) + expect(DCElectricUtils.power(400, 0)).toBe(0) + }) }) diff --git a/tests/utils/StatisticUtils.test.ts b/tests/utils/StatisticUtils.test.ts index 211524b6..7b75a891 100644 --- a/tests/utils/StatisticUtils.test.ts +++ b/tests/utils/StatisticUtils.test.ts @@ -58,4 +58,24 @@ await describe('StatisticUtils', async () => { await it('should calculate standard deviation of array', () => { expect(std([0.25, 4.75, 3.05, 6.04, 1.01, 2.02, 5.03])).toBe(2.1879050645374383) }) + await it('should return 0 for standard deviation of empty or single-element array', () => { + expect(std([])).toBe(0) + expect(std([42])).toBe(0) + }) + await it('should throw TypeError for non-array input to std', () => { + expect(() => std(null as unknown as number[])).toThrow(TypeError) + expect(() => std(undefined as unknown as number[])).toThrow(TypeError) + }) + await it('should throw TypeError for non-array input to percentile', () => { + expect(() => percentile(null as unknown as number[], 50)).toThrow(TypeError) + }) + await it('should throw RangeError for out-of-range percentile', () => { + expect(() => percentile([1, 2, 3], -1)).toThrow(RangeError) + expect(() => percentile([1, 2, 3], 101)).toThrow(RangeError) + }) + await it('should accept pre-computed average parameter', () => { + const data = [0.25, 4.75, 3.05, 6.04, 1.01, 2.02, 5.03] + const avg = average(data) + expect(std(data, avg)).toBe(std(data)) + }) }) diff --git a/tests/utils/Utils.test.ts b/tests/utils/Utils.test.ts index 57d5109a..38d0c691 100644 --- a/tests/utils/Utils.test.ts +++ b/tests/utils/Utils.test.ts @@ -86,35 +86,6 @@ await describe('Utils', async () => { }) await it('should sleep for specified milliseconds using timer mock', async t => { - /** - * Timer mock pattern for testing asynchronous timer-based operations. - * Uses Node.js test module's built-in timer mocking API. - * @example - * // Enable timer mocking with setTimeout API - * t.mock.timers.enable({ apis: ['setTimeout'] }) - * - * try { - * const delay = 10 - * const sleepPromise = sleep(delay) - * // Advance mocked timers by specified milliseconds - * t.mock.timers.tick(delay) - * const timeout = await sleepPromise - * expect(timeout).toBeDefined() - * } finally { - * // Always reset timers after test to prevent side effects - * t.mock.timers.reset() - * } - * @description - * This pattern demonstrates: - * - `t.mock.timers.enable()`: Activates timer mocking for specified APIs (e.g., setTimeout) - * - `t.mock.timers.tick()`: Advances the mocked internal clock by the given milliseconds - * - `t.mock.timers.reset()`: Clears mocked timers; use in finally block to ensure cleanup - * - * Useful for testing: - * - Timeout and interval-based logic - * - Async functions that depend on timing - * - Avoiding slow tests caused by actual delays - */ await withMockTimers(t, ['setTimeout'], async () => { const delay = 10 const sleepPromise = sleep(delay)