@go-go-scope/testing
Comprehensive testing utilities for go-go-scope including mock scopes, spies, time control, and assertion helpers for deterministic testing.
Installation
npm install @go-go-scope/testing
Quick Start
import { createMockScope, expectTask } from '@go-go-scope/testing'
import { describe, test, expect } from 'vitest'
describe('my feature', () => {
test('task succeeds', async () => {
const s = createMockScope()
await expectTask(s.task(() => Promise.resolve('done')))
.toResolveWith('done')
})
})
Mock Scopes
createMockScope()
Create a mock scope with tracking and control capabilities.
import { createMockScope } from '@go-go-scope/testing'
const s = createMockScope({
autoAdvanceTimers: true,
deterministic: true,
services: { db: mockDB },
overrides: { logger: mockLogger }
})
MockScopeOptions
Automatically advance timers for async operations
Use deterministic random seeds for reproducible tests
Pre-configured services to inject
Services to override (for mocking existing services)
Extended scope with mock capabilities
MockScope Properties
Record of task calls made to this scope
Options used to create this mock scope
MockScope Methods
abort()
Manually abort the scope.
const s = createMockScope()
s.abort(new Error('Test abort'))
getTaskCalls()
Get all recorded task calls.
const calls = s.getTaskCalls()
console.log(`Made ${calls.length} task calls`)
Array of task call records
clearTaskCalls()
Clear recorded task calls.
mockService()
Override a service with a mock implementation.
const s = createMockScope()
.mockService('db', mockDB)
.mockService('logger', mockLogger)
Time Control
createTimeController()
Create a time controller for deterministic testing.
import { createTimeController } from '@go-go-scope/testing'
const time = createTimeController()
// Fast forward 10 seconds instantly
time.advance(10000)
// Run all pending timeouts
time.runAll()
Time manipulation controller
TimeController Properties
Current simulated time in milliseconds (read-only)
TimeController Methods
advance()
Advance time by specified amount.
time.advance(5000) // Advance 5 seconds
jumpTo()
Jump to specific absolute time.
time.jumpTo(Date.now() + 10000)
runAll()
Run all pending timeouts immediately.
reset()
Reset time to 0 and clear all pending timeouts.
delay()
Create a Promise that resolves after delay (respects time control).
const promise = time.delay(1000)
time.advance(1000)
await promise // Resolves immediately
install()
Install this controller as global time source.
time.install()
// Now Date.now(), setTimeout, etc. use controlled time
uninstall()
Uninstall and restore original global time.
createTestScope()
Create a scope with time control enabled.
import { createTestScope } from '@go-go-scope/testing'
const { scope, time } = await createTestScope({ timeout: 5000 })
const task = scope.task(() => longOperation(), { timeout: 10000 })
// Fast forward past timeout
time.advance(10001)
const [err] = await task
expect(err?.message).toContain('timeout')
options
{ timeout?: number; concurrency?: number }
Scope options
{ scope: Scope; time: TimeController }
Scope and time controller
createTimeTravelController()
Advanced time controller with timeline inspection.
import { createTimeTravelController } from '@go-go-scope/testing'
const time = createTimeTravelController()
const results: number[] = []
time.setTimeout(() => results.push(1), 100)
time.setTimeout(() => results.push(2), 200)
// Jump to specific time
time.jumpTo(150)
expect(results).toEqual([1])
// Continue
time.advance(100)
expect(results).toEqual([1, 2])
// Inspect timeline
time.printTimeline()
Advanced time controller with history
Assertions
expectTask()
Create assertion helpers for a task.
import { expectTask } from '@go-go-scope/testing'
await expectTask(task)
.toResolve()
await expectTask(task)
.toResolveWith('expected value')
await expectTask(task)
.toReject()
await expectTask(task)
.toRejectWith('error message')
task
Task<Result<Error, T>>
required
Task to assert
Assertion helper with chainable methods
TaskAssertion Methods
toResolve()
Assert task resolves without error.
await expectTask(task).toResolve()
toResolveWith()
Assert task resolves with specific value.
await expectTask(task).toResolveWith('expected')
toResolveWithin()
Assert task resolves within timeout.
await expectTask(task)
.toResolveWithin(1000)
.toResolveWith('done')
toReject()
Assert task rejects with error.
await expectTask(task).toReject()
toRejectWith()
Assert task rejects with specific error message.
await expectTask(task).toRejectWith('fail')
await expectTask(task).toRejectWith(/error/i)
Expected error message or pattern
toRejectWithType()
Assert task fails with specific error type.
await expectTask(task).toRejectWithType(TypeError)
errorClass
new (...args: any[]) => Error
required
Error class constructor
result()
Get the result tuple for manual assertions.
const [err, value] = await expectTask(task).result()
assertResolves()
Assert that a task resolves successfully.
import { assertResolves } from '@go-go-scope/testing'
const [err, result] = await assertResolves(task)
expect(result).toBe('done')
task
Task<Result<Error, T>>
required
Task to assert
Result tuple (will throw if task rejects)
assertRejects()
Assert that a task rejects with an error.
import { assertRejects } from '@go-go-scope/testing'
const err = await assertRejects(task)
expect(err.message).toBe('fail')
task
Task<Result<Error, T>>
required
Task to assert
Error from task (will throw if task resolves)
assertResolvesWithin()
Assert that a task resolves within a timeout.
import { assertResolvesWithin } from '@go-go-scope/testing'
const [err, result] = await assertResolvesWithin(task, 1000)
task
Task<Result<Error, T>>
required
Task to assert
assertScopeDisposed()
Assert that a scope has been properly disposed.
import { assertScopeDisposed } from '@go-go-scope/testing'
const s = scope()
s.task(() => doSomething())
await assertScopeDisposed(s)
Spies
createSpy()
Create a spy function for tracking calls.
import { createSpy } from '@go-go-scope/testing'
const spy = createSpy()
.mockReturnValue('mocked')
const result = spy('arg1', 'arg2')
expect(result).toBe('mocked')
expect(spy.wasCalled()).toBe(true)
expect(spy.wasCalledWith('arg1', 'arg2')).toBe(true)
Spy function with tracking
Spy Properties
calls
Array<{ args: TArgs; result: TReturn }>
Record of all calls
Spy Methods
mockImplementation()
Set mock implementation.
const spy = createSpy()
.mockImplementation((x: number) => x * 2)
fn
(...args: TArgs) => TReturn
required
Mock implementation function
mockReturnValue()
Set mock return value.
const spy = createSpy().mockReturnValue('result')
mockReset()
Reset spy state.
getCalls()
Get all call records.
const calls = spy.getCalls()
wasCalled()
Check if spy was called.
if (spy.wasCalled()) {
console.log('Spy was called')
}
wasCalledWith()
Check if spy was called with specific arguments.
if (spy.wasCalledWith('arg1', 'arg2')) {
console.log('Called with expected args')
}
Mock Channels
createMockChannel()
Create a mock channel for testing.
import { createMockChannel } from '@go-go-scope/testing'
const mockCh = createMockChannel<number>()
mockCh.setReceiveValues([1, 2, 3])
const ch = mockCh.channel
const value = await ch.receive()
expect(value).toBe(1)
Mock channel with control methods
MockChannel Properties
Whether channel is closed
MockChannel Methods
setReceiveValues()
Set values to be received.
mockCh.setReceiveValues([1, 2, 3])
mockReceive()
Simulate receiving a value.
getSentValues()
Get all sent values.
const sent = mockCh.getSentValues()
clearSentValues()
Clear sent values.
close()
Close the mock channel.
reset()
Reset to initial state.
Utilities
flushPromises()
Wait for all promises to settle.
import { flushPromises } from '@go-go-scope/testing'
const promise = s.task(async () => {
await delay(100)
return 'done'
})
await flushPromises()
const [err, result] = await promise
expect(result).toBe('done')
createControlledTimer()
Create a simple controlled timer environment.
import { createControlledTimer } from '@go-go-scope/testing'
const timer = createControlledTimer()
timer.setTimeout(() => console.log('fired'), 1000)
timer.advance(1000)
Controlled timer with advance/flush methods
Examples
Testing Timeouts
import { createTestScope, expectTask } from '@go-go-scope/testing'
import { describe, test, expect } from 'vitest'
describe('timeout behavior', () => {
test('task times out', async () => {
const { scope, time } = await createTestScope({ timeout: 5000 })
const task = scope.task(
() => new Promise(resolve => setTimeout(resolve, 10000)),
{ timeout: 5000 }
)
// Fast forward past timeout
time.advance(5001)
await expectTask(task).toReject()
})
})
Testing Retries
import { createMockScope } from '@go-go-scope/testing'
test('task retries', async () => {
const s = createMockScope()
let attempts = 0
const [err, result] = await s.task(() => {
attempts++
if (attempts < 3) throw new Error('fail')
return 'success'
}, { retry: { maxRetries: 3, delay: 0 } })
expect(err).toBeUndefined()
expect(result).toBe('success')
expect(attempts).toBe(3)
})
Mocking Services
import { createMockScope, createSpy } from '@go-go-scope/testing'
test('uses injected service', async () => {
const mockDB = {
query: createSpy().mockReturnValue([{ id: 1 }])
}
const s = createMockScope({ services: { db: mockDB } })
const [err, users] = await s.task(({ services }) => {
return services.db.query('SELECT * FROM users')
})
expect(err).toBeUndefined()
expect(users).toEqual([{ id: 1 }])
expect(mockDB.query.wasCalled()).toBe(true)
})
Testing Cancellation
import { createMockScope } from '@go-go-scope/testing'
test('cancels on scope disposal', async () => {
const s = createMockScope()
let aborted = false
const task = s.task(({ signal }) => {
return new Promise((_, reject) => {
signal.addEventListener('abort', () => {
aborted = true
reject(new Error('aborted'))
})
})
})
s.abort()
const [err] = await task
expect(err?.message).toBe('aborted')
expect(aborted).toBe(true)
})
Testing Channels
import { createMockChannel } from '@go-go-scope/testing'
test('channel communication', async () => {
const mockCh = createMockChannel<number>()
mockCh.setReceiveValues([1, 2, 3])
const ch = mockCh.channel
await ch.send(10)
await ch.send(20)
expect(mockCh.getSentValues()).toEqual([10, 20])
const values: number[] = []
for await (const value of ch) {
values.push(value)
}
expect(values).toEqual([1, 2, 3])
})