Skip to main content

@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 }
})
options
MockScopeOptions
Mock scope configuration

MockScopeOptions

autoAdvanceTimers
boolean
Automatically advance timers for async operations
deterministic
boolean
Use deterministic random seeds for reproducible tests
services
Record<string, unknown>
Pre-configured services to inject
overrides
Record<string, unknown>
Services to override (for mocking existing services)
aborted
boolean
Initial aborted state
abortReason
unknown
Abort reason if aborted
MockScope
MockScope
Extended scope with mock capabilities

MockScope Properties

taskCalls
TaskCall[]
Record of task calls made to this scope
options
MockScopeOptions
Options used to create this mock scope

MockScope Methods

abort()

Manually abort the scope.
const s = createMockScope()
s.abort(new Error('Test abort'))
reason
unknown
Abort reason

getTaskCalls()

Get all recorded task calls.
const calls = s.getTaskCalls()
console.log(`Made ${calls.length} task calls`)
TaskCall[]
TaskCall[]
Array of task call records

clearTaskCalls()

Clear recorded task calls.
s.clearTaskCalls()

mockService()

Override a service with a mock implementation.
const s = createMockScope()
  .mockService('db', mockDB)
  .mockService('logger', mockLogger)
key
string
required
Service key
value
T
required
Mock service value

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()
TimeController
TimeController
Time manipulation controller

TimeController Properties

now
number
Current simulated time in milliseconds (read-only)

TimeController Methods

advance()

Advance time by specified amount.
time.advance(5000) // Advance 5 seconds
ms
number
required
Milliseconds to advance

jumpTo()

Jump to specific absolute time.
time.jumpTo(Date.now() + 10000)
timestamp
number
required
Target timestamp

runAll()

Run all pending timeouts immediately.
time.runAll()

reset()

Reset time to 0 and clear all pending timeouts.
time.reset()

delay()

Create a Promise that resolves after delay (respects time control).
const promise = time.delay(1000)
time.advance(1000)
await promise // Resolves immediately
ms
number
required
Delay in milliseconds

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.
time.uninstall()

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 }
object
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()
TimeTravelController
object
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
TaskAssertion<T>
TaskAssertion<T>
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')
expected
T
required
Expected value

toResolveWithin()

Assert task resolves within timeout.
await expectTask(task)
  .toResolveWithin(1000)
  .toResolveWith('done')
timeoutMs
number
required
Timeout in milliseconds

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)
message
string | RegExp
required
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<Error, T>
Result<Error, T>
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
Error
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
timeoutMs
number
required
Timeout in milliseconds

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)
scope
Scope
required
Scope to check

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<TArgs, TReturn>
Spy<TArgs, TReturn>
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')
value
TReturn
required
Return value

mockReset()

Reset spy state.
spy.mockReset()

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')
}
...expectedArgs
TArgs
required
Expected arguments

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)
MockChannel<T>
MockChannel<T>
Mock channel with control methods

MockChannel Properties

channel
Channel<T>
The channel instance
sentValues
T[]
Record of sent values
isClosed
boolean
Whether channel is closed

MockChannel Methods

setReceiveValues()

Set values to be received.
mockCh.setReceiveValues([1, 2, 3])

mockReceive()

Simulate receiving a value.
mockCh.mockReceive(42)

getSentValues()

Get all sent values.
const sent = mockCh.getSentValues()

clearSentValues()

Clear sent values.
mockCh.clearSentValues()

close()

Close the mock channel.
mockCh.close()

reset()

Reset to initial state.
mockCh.reset()

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)
ControlledTimer
object
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])
})

Build docs developers (and LLMs) love