Detalhes do pacote

@universal-packages/tests-runner

Tests runner and utilities.

readme (leia-me)

Tests Runner

npm version Testing codecov

Tests runner and utilities.

Getting Started

npm install @universal-packages/tests-runner

Usage

TestsRunner class

The TestsRunner class is a utility where you pack and organize your tests. It also supplies all matchers and utilities for testing.

import { TestsRunner } from '@universal-packages/tests-runner'

import { MyCode } from './my-code'

const testsRunner = new TestsRunner()

testsRunner.describe(MyCode, () => {
  testsRunner.it('should do something', () => {
    const result = MyCode.doSomething()
    testsRunner.expect(result).toEqual('expected result')
  })
})

const testResults = await testsRunner.run()

Constructor constructor

new TestsRunner(options?: TestsRunnerOptions)

TestsRunnerOptions

Additional to the BaseRunnerOptions, the following options are available:

  • bail: boolean (default: false) If a test fails, the tests-runner will stop and mark the test suite as failed.
  • runOrder: sequence | random | parallel (default: sequence) It defines the order of the tests.
    • sequence: Run tests in the order they are defined.
    • random: Run tests in random order.
    • parallel: Run tests in parallel.
  • testTimeout: number (default: 5000) If the test takes longer than the timeout, it will be killed and marked as failed.

Tests description

describe

describe(description: string, callback: () => void, options?: DescribeOptions)

The describe method is used to group tests together.

testsRunner.describe(MyCode, () => {
  testsRunner.describe('when passing a string', () => {
    testsRunner.it('should do something', () => {
      const result = MyCode.doSomething('test')
      testsRunner.expect(result).toEqual('string found')
    })

    testsRunner.describe('when passing an array', () => {
      testsRunner.it('should do something', () => {
        const result = MyCode.doSomething(['test'])
        testsRunner.expect(result).toEqual('array found')
      })
    })
  })
})
DescribeOptions
  • only: boolean (default: false) It will run only the tests that are inside the describe block.
  • skip: boolean (default: false) It will skip the tests that are inside the describe block.
  • skipReason: string (default: null) Information about why the tests are skipped.
  • timeout: number (default: 5000) If the test inside the describe block takes longer than the timeout, it will be killed and marked as failed.

when

when(description: string, callback: () => void)

The when is an alias for the describe method.

test

test(description: string, callback: () => void)

The test method is used to run a single test. It can be inside a describe block or by itself.

tests -
  runner.test('should do something', () => {
    const result = MyCode.doSomething()
    tests - runner.expect(result).toEqual('expected result')
  })
TestOptions
  • only: boolean (default: false) It will run only the tests that are inside the describe block.
  • skip: boolean (default: false) It will skip the tests that are inside the describe block.
  • skipReason: string (default: null) Information about why the tests are skipped.
  • timeout: number (default: 5000) If the test takes longer than the timeout, it will be killed and marked as failed.

it

it(description: string, callback: () => void)

The it method is an alias for the test method.

Tests lifecycle

before

before(callback: () => void | Promise<void>)

The before method is used to run a function before the immediate tests are run. For example, if you need something to happen before the whole test suite is run, you can use the before method outside of all the describe blocks.

testsRunner.before(() => {
  console.log('before the whole test suite')
})

If you want to run something just before a group of tests inside a describe block, you can use the before method inside the describe block.

testsRunner.describe('when passing a string', () => {
  testsRunner.before(() => {
    console.log('before the tests inside the describe block')
  })
})

beforeEach

beforeEach(callback: () => void | Promise<void>)

The beforeEach method is used to run a function before each test. Just like the before method, it can be inside a describe block or by itself. If it is outside of a describe block, it will run before each test.

testsRunner.beforeEach(() => {
  console.log('before each test')
})

If it is inside a describe block, it will run before each test inside that describe block.

testsRunner.describe('when passing a string', () => {
  testsRunner.beforeEach(() => {
    console.log('before each test inside the describe block')
  })
})

after

after(callback: () => void | Promise<void>)

The after method is used to run a function after the immediate tests are run. For example, if you need something to happen after the whole test suite is run, you can use the after method outside of all the describe blocks.

testsRunner.after(() => {
  console.log('after the whole test suite')
})

If you want to run something just after a group of tests inside a describe block, you can use the after method inside the describe block.

testsRunner.describe('when passing a string', () => {
  testsRunner.after(() => {
    console.log('after the tests inside the describe block')
  })
})

afterEach

afterEach(callback: () => void | Promise<void>)

The afterEach method is used to run a function after each test. Just like the after method, it can be inside a describe block or by itself. If it is outside of a describe block, it will run after each test.

testsRunner.afterEach(() => {
  console.log('after each test inside the describe block')
})

If it is inside a describe block, it will run after each test inside that describe block.

testsRunner.describe('when passing a string', () => {
  testsRunner.afterEach(() => {
    console.log('after each test inside the describe block')
  })
})

Test matchers

The TestsRunner class has a set of matchers that can be used to test the value of a variable.

expect

expect(value: any)

The expect method is used to setup an assertion with the obtained value of some calculation.

testsRunner.expect(result).toEqual('expected result')

toBe

toBe(value: any)

The toBe matcher is used to test if the value of a variable is equal to the expected value. It works the same way as the toEqual matcher for primitive values, but it will not work for objects and arrays. For an object or array, it will check if the value is the same instance of the expected value.

tests - runner.expect(result).toBe(expectedResult)

toBeCloseTo

toBeCloseTo(number: number, precision?: number)

The toBeCloseTo matcher is used to test if a floating point number is close to the expected value within a certain precision. The default precision is 2 decimal places.

testsRunner.expect(0.1 + 0.2).toBeCloseTo(0.3)
testsRunner.expect(0.1 + 0.2).toBeCloseTo(0.3, 5)

toBeDefined

toBeDefined()

The toBeDefined matcher is used to test if the value is not undefined.

testsRunner.expect(result).toBeDefined()

toBeFalsy

toBeFalsy()

The toBeFalsy matcher is used to test if the value is falsy (evaluates to false in a boolean context).

testsRunner.expect(result).toBeFalsy()

toBeGreaterThan

toBeGreaterThan(number: number)

The toBeGreaterThan matcher is used to test if a number is greater than the expected value.

testsRunner.expect(10).toBeGreaterThan(5)

toBeGreaterThanOrEqual

toBeGreaterThanOrEqual(number: number)

The toBeGreaterThanOrEqual matcher is used to test if a number is greater than or equal to the expected value.

testsRunner.expect(10).toBeGreaterThanOrEqual(10)
testsRunner.expect(15).toBeGreaterThanOrEqual(10)

toBeInstanceOf

toBeInstanceOf(constructor: Function)

The toBeInstanceOf matcher is used to test if an object is an instance of a specific constructor function or class.

testsRunner.expect(new Date()).toBeInstanceOf(Date)
testsRunner.expect([]).toBeInstanceOf(Array)
testsRunner.expect('hello').toBeInstanceOf(String)

toBeLessThan

toBeLessThan(number: number)

The toBeLessThan matcher is used to test if a number is less than the expected value.

testsRunner.expect(5).toBeLessThan(10)

toBeLessThanOrEqual

toBeLessThanOrEqual(number: number)

The toBeLessThanOrEqual matcher is used to test if a number is less than or equal to the expected value.

testsRunner.expect(10).toBeLessThanOrEqual(10)
testsRunner.expect(5).toBeLessThanOrEqual(10)

toBeNaN

toBeNaN()

The toBeNaN matcher is used to test if the value is NaN.

testsRunner.expect(NaN).toBeNaN()
testsRunner.expect(Number('not a number')).toBeNaN()

toBeNull

toBeNull()

The toBeNull matcher is used to test if the value is null.

testsRunner.expect(result).toBeNull()

toBeTruthy

toBeTruthy()

The toBeTruthy matcher is used to test if the value is truthy (evaluates to true in a boolean context).

testsRunner.expect(result).toBeTruthy()

toBeUndefined

toBeUndefined()

The toBeUndefined matcher is used to test if the value is undefined.

testsRunner.expect(result).toBeUndefined()

toContain

toContain(item: any)

The toContain matcher is used to test if an array contains a specific item or if a string contains a substring.

testsRunner.expect(['apple', 'banana', 'cherry']).toContain('banana')
testsRunner.expect('hello world').toContain('world')

toContainEqual

toContainEqual(item: any)

The toContainEqual matcher is used to test if an array contains an item that is equal to the expected value (using deep equality comparison).

testsRunner.expect([{ name: 'John' }, { name: 'Jane' }]).toContainEqual({ name: 'John' })

toEqual

toEqual(value: any)

The toEqual matcher is used to test if the value of a variable is equal to the expected value. It works for primitive values, arrays, and objects. For objects, it will check if the value is equal to the expected value by comparing the keys and values. For arrays, it will check if the value is equal to the expected value by comparing the length and the values of the array.

testsRunner.expect(result).toEqual('expected result')

toHaveLength

toHaveLength(length: number)

The toHaveLength matcher is used to test if an array, string, or any object with a length property has the expected length.

testsRunner.expect([1, 2, 3]).toHaveLength(3)
testsRunner.expect('hello').toHaveLength(5)

toHaveProperty

toHaveProperty(path: string, value?: any)

The toHaveProperty matcher is used to test if an object has a specific property. Optionally, you can also check if the property has a specific value. The path can use dot notation for nested properties.

testsRunner.expect({ name: 'John', age: 30 }).toHaveProperty('name')
testsRunner.expect({ name: 'John', age: 30 }).toHaveProperty('name', 'John')
testsRunner.expect({ user: { name: 'John' } }).toHaveProperty('user.name', 'John')

toMatch

toMatch(regex: RegExp)

The toMatch matcher is used to test if a string matches a regular expression.

testsRunner.expect('hello world').toMatch(/world/)
testsRunner.expect('123-456-7890').toMatch(/\d{3}-\d{3}-\d{4}/)

toMatchObject

toMatchObject(object: Record<string, any>)

The toMatchObject matcher is used to test if an object matches a subset of properties from the expected object. The actual object can have additional properties.

testsRunner.expect({ name: 'John', age: 30, city: 'New York' }).toMatchObject({ name: 'John', age: 30 })

toReject

async toReject(expected?: string | RegExp | Error): Promise<void>

The toReject matcher is used to test if a promise rejects. You can optionally specify the expected error message, error instance, or a regex pattern.

await testsRunner.expect(Promise.reject(new Error('Failed'))).toReject()
await testsRunner.expect(Promise.reject(new Error('Failed'))).toReject('Failed')
await testsRunner.expect(Promise.reject(new Error('Failed'))).toReject(/Failed/)

toResolve

async toResolve(expected?: any): Promise<void>

The toResolve matcher is used to test if a promise resolves successfully. You can optionally specify the expected resolved value.

await testsRunner.expect(Promise.resolve('success')).toResolve()
await testsRunner.expect(Promise.resolve('success')).toResolve('success')

toThrow

toThrow(expected?: Error | RegExp | string)

The toThrow matcher is used to test if a function throws an error. You can optionally specify the expected error message, error instance, or a regex pattern.

testsRunner
  .expect(() => {
    throw new Error('Something went wrong')
  })
  .toThrow()
testsRunner
  .expect(() => {
    throw new Error('Something went wrong')
  })
  .toThrow('Something went wrong')
testsRunner
  .expect(() => {
    throw new Error('Something went wrong')
  })
  .toThrow(/went wrong/)
testsRunner
  .expect(() => {
    throw new Error('Something went wrong')
  })
  .toThrow(new Error('Something went wrong'))

Mock function matchers

The following matchers are used specifically for testing mock functions created with the testing framework.

toHaveBeenCalled

toHaveBeenCalled()

The toHaveBeenCalled matcher is used to test if a mock function has been called at least once.

const mockFn = testsRunner.mockFn()
mockFn()
testsRunner.expect(mockFn).toHaveBeenCalled()

toHaveBeenCalledTimes

toHaveBeenCalledTimes(expectedCount: number)

The toHaveBeenCalledTimes matcher is used to test if a mock function has been called a specific number of times.

const mockFn = testsRunner.mockFn()
mockFn()
mockFn()
testsRunner.expect(mockFn).toHaveBeenCalledTimes(2)

toHaveBeenCalledTimesWith

toHaveBeenCalledTimesWith(expectedCount: number, ...args: any[])

The toHaveBeenCalledTimesWith matcher is used to test if a mock function has been called a specific number of times with specific arguments.

const mockFn = testsRunner.mockFn()
mockFn('hello')
mockFn('world')
mockFn('hello')
testsRunner.expect(mockFn).toHaveBeenCalledTimesWith(2, 'hello')

toHaveBeenCalledWith

toHaveBeenCalledWith(...args: any[])

The toHaveBeenCalledWith matcher is used to test if a mock function has been called with specific arguments.

const mockFn = testsRunner.mockFn()
mockFn('hello', 'world')
testsRunner.expect(mockFn).toHaveBeenCalledWith('hello', 'world')

toHaveBeenLastCalledWith

toHaveBeenLastCalledWith(...args: any[])

The toHaveBeenLastCalledWith matcher is used to test if a mock function's last call was made with specific arguments.

const mockFn = testsRunner.mockFn()
mockFn('first', 'call')
mockFn('last', 'call')
testsRunner.expect(mockFn).toHaveBeenLastCalledWith('last', 'call')

toHaveBeenNthCalledWith

toHaveBeenNthCalledWith(n: number, ...args: any[])

The toHaveBeenNthCalledWith matcher is used to test if a mock function's nth call was made with specific arguments.

const mockFn = testsRunner.mockFn()
mockFn('first')
mockFn('second')
mockFn('third')
testsRunner.expect(mockFn).toHaveBeenNthCalledWith(2, 'second')

Mock function creation

mockFn

mockFn(): MockFn

The mockFn method creates a mock function that can be used for testing. Mock functions allow you to track calls, control return values, and implement custom behavior for testing purposes.

const mockFn = testsRunner.mockFn()
Basic Usage

By default, a mock function returns undefined when called:

const mockFn = testsRunner.mockFn()
testsRunner.expect(mockFn()).toBeUndefined()
implement
implement(fn: Function): void

Sets a permanent implementation for the mock function:

const mockFn = testsRunner.mockFn()
mockFn.implement((a: number, b: number) => a + b)
testsRunner.expect(mockFn(1, 2)).toBe(3)
testsRunner.expect(mockFn(3, 4)).toBe(7)
implementOnce
implementOnce(fn: Function): void

Sets a one-time implementation for the mock function. After the first call, subsequent calls will use the next implementOnce or fall back to the default behavior:

const mockFn = testsRunner.mockFn()
mockFn.implementOnce(() => 'first call')
mockFn.implementOnce(() => 'second call')
testsRunner.expect(mockFn()).toBe('first call')
testsRunner.expect(mockFn()).toBe('second call')
testsRunner.expect(mockFn()).toBeUndefined()
scenario
scenario(args: any[], returnValue: any): void

Sets up scenario-based return values. When the mock function is called with arguments that match the scenario, it returns the specified value:

const mockFn = testsRunner.mockFn()
mockFn.scenario([1, 2], 'one and two')
mockFn.scenario(['hello'], 'greeting')
mockFn.scenario([{ a: 1, b: 2 }], 'object match')

testsRunner.expect(mockFn(1, 2)).toBe('one and two')
testsRunner.expect(mockFn('hello')).toBe('greeting')
testsRunner.expect(mockFn({ a: 1, b: 2 })).toBe('object match')
testsRunner.expect(mockFn('other')).toBeUndefined()
calls
calls: Array<{ args: any[] }>

An array that tracks all calls made to the mock function, including the arguments passed:

const mockFn = testsRunner.mockFn()
mockFn('first')
mockFn('second', 123)
mockFn({ complex: 'object' })

testsRunner.expect(mockFn.calls.length).toBe(3)
testsRunner.expect(mockFn.calls[0].args).toEqual(['first'])
testsRunner.expect(mockFn.calls[1].args).toEqual(['second', 123])
testsRunner.expect(mockFn.calls[2].args[0]).toEqual({ complex: 'object' })
reset
reset(): void

Resets the mock function, clearing all call history, implementations, and scenarios:

const mockFn = testsRunner.mockFn()
mockFn.implement(() => 'implementation')
mockFn.scenario([1], 'one')
mockFn()

mockFn.reset()

testsRunner.expect(mockFn.calls.length).toBe(0)
testsRunner.expect(mockFn()).toBeUndefined()
testsRunner.expect(mockFn(1)).toBeUndefined() // Scenario is gone

Mock functions work seamlessly with all the mock function matchers like toHaveBeenCalled, toHaveBeenCalledWith, toHaveBeenCalledTimes, etc.

spyOn

spyOn(object: any, propertyPath: string): SpyFn

The spyOn method creates a spy for an existing method on an object. Unlike mockFn, spies preserve the original behavior of the method while allowing you to track calls and optionally override the implementation.

const obj = { getValue: () => 'original' }
const spy = testsRunner.spyOn(obj, 'getValue')
Basic Spying

By default, spying preserves the original method behavior while tracking all calls:

class Calculator {
  sum(a: number, b: number): number {
    return a + b
  }
}

const calculator = new Calculator()
const spy = testsRunner.spyOn(calculator, 'sum')

const result = calculator.sum(1, 2)
testsRunner.expect(result).toBe(3) // Original behavior preserved
testsRunner.expect(spy.calls).toHaveLength(1)
testsRunner.expect(spy.calls[0].args).toEqual([1, 2])
testsRunner.expect(spy.calls[0].result).toBe(3)
Call Tracking

Spies automatically track all method calls with their arguments and return values:

const obj = { multiply: (x: number, y: number) => x * y }
const spy = testsRunner

obj.multiply(3, 4)
obj.multiply(5, 6)

testsRunner.expect(spy.calls).toHaveLength(2)
testsRunner.expect(spy.calls[0].args).toEqual([3, 4])
testsRunner.expect(spy.calls[0].result).toBe(12)
testsRunner.expect(spy.calls[1].args).toEqual([5, 6])
testsRunner.expect(spy.calls[1].result).toBe(30)
implement
implement(fn: Function): void

Override the original method implementation permanently:

const obj = { getValue: () => 'original' }
const spy = testsRunner.spyOn(obj, 'getValue')

spy.implement(() => 'mocked')

testsRunner.expect(obj.getValue()).toBe('mocked')
testsRunner.expect(spy.calls[0].result).toBe('mocked')
implementOnce
implementOnce(fn: Function): void

Override the method implementation for one call only, then fall back to original behavior:

const obj = { getValue: () => 'original' }
const spy = testsRunner.spyOn(obj, 'getValue')

spy.implementOnce(() => 'once')

testsRunner.expect(obj.getValue()).toBe('once') // First call uses override
testsRunner.expect(obj.getValue()).toBe('original') // Second call uses original
scenario
scenario(args: any[], returnValue: any): void

Return specific values when called with matching arguments, otherwise use original behavior:

const obj = { add: (a: number, b: number) => a + b }
const spy = testsRunner.spyOn(obj, 'add')

spy.scenario([1, 2], 100)

testsRunner.expect(obj.add(1, 2)).toBe(100) // Scenario match
testsRunner.expect(obj.add(3, 4)).toBe(7) // Original behavior
restore
restore(): void

Restore the original method implementation and stop spying:

const obj = { getValue: () => 'original' }
const spy = testsRunner.spyOn(obj, 'getValue')

spy.implement(() => 'mocked')
testsRunner.expect(obj.getValue()).toBe('mocked')

spy.restore()
testsRunner.expect(obj.getValue()).toBe('original')
Context Preservation

Spies preserve the this context, ensuring methods work correctly with object state:

class Counter {
  count: number = 0

  increment(): number {
    this.count++
    return this.count
  }
}

const counter = new Counter()
const spy = testsRunner.spyOn(counter, 'increment')

testsRunner.expect(counter.increment()).toBe(1)
testsRunner.expect(counter.count).toBe(1) // State correctly modified
testsRunner.expect(spy.calls[0].result).toBe(1)

Even with custom implementations, this context is preserved:

const spy = testsRunner.spyOn(counter, 'increment')
spy.implement(function (this: Counter) {
  this.count += 2 // Custom behavior with correct context
  return this.count
})
Error Handling

Attempting to spy on non-existent properties throws an error:

const obj = {}
testsRunner
  .expect(() => {
    testsRunner.spyOn(obj, 'nonExistent')
  })
  .toThrow('Cannot spy on nonExistent: property does not exist')

Spies work seamlessly with all mock function matchers like toHaveBeenCalled, toHaveBeenCalledWith, toHaveBeenCalledTimes, etc.

Asymmetric assertions

Asymmetric assertions are special objects that allow you to perform partial matching within complex data structures. They are particularly useful when testing objects or arrays where you only care about certain properties or values matching specific criteria, rather than exact equality.

Asymmetric assertions work with matchers like toEqual, toMatchObject, toHaveBeenCalledWith, and others, allowing you to specify flexible matching rules for nested values.

Basic Usage

Instead of requiring exact matches, you can use asymmetric assertions to test specific conditions:

// Instead of exact matching
testsRunner.expect({ id: 1, name: 'John', timestamp: 1634567890 }).toEqual({
  id: 1,
  name: 'John',
  timestamp: 1634567890 // Hard to predict exact timestamp
})

// Use asymmetric assertions for flexible matching
testsRunner.expect({ id: 1, name: 'John', timestamp: 1634567890 }).toEqual({
  id: 1,
  name: testsRunner.expectAnything(),
  timestamp: testsRunner.expectGreaterThan(1634567000)
})

expectAnything

expectAnything()

Matches any value, including null, undefined, and NaN:

testsRunner.expect({ a: 1, b: 'hello', c: null }).toEqual({
  a: 1,
  b: testsRunner.expectAnything(),
  c: testsRunner.expectAnything()
})

testsRunner.expect([1, 'hello', null]).toEqual([1, testsRunner.expectAnything(), testsRunner.expectAnything()])

expectGreaterThan

expectGreaterThan(value: number)

Matches numbers greater than the specified value:

testsRunner.expect({ score: 85, count: 20 }).toEqual({
  score: testsRunner.expectGreaterThan(80),
  count: testsRunner.expectGreaterThan(10)
})

expectLessThan

expectLessThan(value: number)

Matches numbers less than the specified value:

testsRunner.expect({ temperature: 15, humidity: 45 }).toEqual({
  temperature: testsRunner.expectLessThan(20),
  humidity: testsRunner.expectLessThan(50)
})

expectGreaterThanOrEqual

expectGreaterThanOrEqual(value: number)

Matches numbers greater than or equal to the specified value:

testsRunner.expect({ min: 10, max: 100 }).toEqual({
  min: testsRunner.expectGreaterThanOrEqual(10),
  max: testsRunner.expectGreaterThanOrEqual(50)
})

expectLessThanOrEqual

expectLessThanOrEqual(value: number)

Matches numbers less than or equal to the specified value:

testsRunner.expect({ limit: 100, current: 75 }).toEqual({
  limit: testsRunner.expectLessThanOrEqual(100),
  current: testsRunner.expectLessThanOrEqual(100)
})

expectCloseTo

expectCloseTo(value: number, precision?: number)

Matches floating-point numbers that are close to the expected value within a specified precision:

testsRunner.expect({ result: 10.001, pi: 3.14159 }).toEqual({
  result: testsRunner.expectCloseTo(10, 2),
  pi: testsRunner.expectCloseTo(3.14, 2)
})

expectMatch

expectMatch(pattern: RegExp)

Matches strings that match the provided regular expression:

testsRunner.expect({ email: 'user@example.com', phone: '123-456-7890' }).toEqual({
  email: testsRunner.expectMatch(/@example\.com$/),
  phone: testsRunner.expectMatch(/^\d{3}-\d{3}-\d{4}$/)
})

expectInstanceOf

expectInstanceOf(constructor: Function)

Matches values that are instances of the specified constructor or class:

testsRunner.expect({ created: new Date(), error: new Error('test') }).toEqual({
  created: testsRunner.expectInstanceOf(Date),
  error: testsRunner.expectInstanceOf(Error)
})

expectContain

expectContain(item: any)

Matches strings containing a substring or arrays containing a specific value:

testsRunner.expect({ message: 'hello world', tags: ['important', 'test'] }).toEqual({
  message: testsRunner.expectContain('world'),
  tags: testsRunner.expectContain('important')
})

expectContainEqual

expectContainEqual(item: any)

Matches arrays containing an object that deeply equals the expected value:

testsRunner
  .expect({
    users: [
      { id: 1, name: 'John' },
      { id: 2, name: 'Jane' }
    ]
  })
  .toEqual({
    users: testsRunner.expectContainEqual({ id: 1, name: 'John' })
  })

expectHaveLength

expectHaveLength(length: number)

Matches arrays or strings with the specified length:

testsRunner.expect({ items: [1, 2, 3], title: 'hello' }).toEqual({
  items: testsRunner.expectHaveLength(3),
  title: testsRunner.expectHaveLength(5)
})

expectHaveProperty

expectHaveProperty(path: string, value?: any)

Matches objects that have the specified property, optionally with a specific value:

testsRunner.expect({ user: { name: 'John', age: 30 } }).toEqual({
  user: testsRunner.expectHaveProperty('name')
})

testsRunner.expect({ config: { enabled: true, timeout: 5000 } }).toEqual({
  config: testsRunner.expectHaveProperty('enabled', true)
})

expectMatchObject

expectMatchObject(obj: Record<string, any>)

Matches objects that contain at least the specified properties and values:

testsRunner
  .expect({
    user: { id: 1, name: 'John', email: 'john@example.com', role: 'admin' }
  })
  .toEqual({
    user: testsRunner.expectMatchObject({ name: 'John', role: 'admin' })
  })

expectTruthy

expectTruthy()

Matches any truthy value:

testsRunner.expect({ active: true, count: 5, name: 'test' }).toEqual({
  active: testsRunner.expectTruthy(),
  count: testsRunner.expectTruthy(),
  name: testsRunner.expectTruthy()
})

expectFalsy

expectFalsy()

Matches any falsy value (false, 0, '', null, undefined, NaN):

testsRunner.expect({ disabled: false, count: 0, name: '' }).toEqual({
  disabled: testsRunner.expectFalsy(),
  count: testsRunner.expectFalsy(),
  name: testsRunner.expectFalsy()
})

Negated Asymmetric Assertions

All asymmetric assertions support negation using the not property:

testsRunner.expect({ score: 5, message: 'hello' }).toEqual({
  score: testsRunner.not.expectGreaterThan(10),
  message: testsRunner.not.expectMatch(/world/)
})

Complex Examples

Asymmetric assertions can be combined for sophisticated testing scenarios:

const apiResponse = {
  id: 123,
  name: 'Test Object',
  created: new Date(),
  tags: ['important', 'test', 'example'],
  metadata: {
    version: '1.0.5',
    priority: 1,
    settings: {
      enabled: true,
      timeout: 500
    }
  },
  items: [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' }
  ]
}

testsRunner.expect(apiResponse).toEqual({
  id: testsRunner.expectGreaterThan(100),
  name: testsRunner.expectMatch(/Test/),
  created: testsRunner.expectInstanceOf(Date),
  tags: testsRunner.expectContain('important'),
  metadata: testsRunner.expectMatchObject({
    version: testsRunner.expectMatch(/^\d+\.\d+\.\d+$/),
    priority: testsRunner.expectLessThanOrEqual(10),
    settings: testsRunner.expectHaveProperty('enabled', true)
  }),
  items: testsRunner.expectHaveLength(3)
})

Asymmetric assertions work seamlessly with mock function matchers:

const mockFn = tests - runner.mockFn()
mockFn({ timestamp: Date.now(), data: 'test' })

testsRunner.expect(mockFn).toHaveBeenCalledWith({
  timestamp: testsRunner.expectGreaterThan(0),
  data: testsRunner.expectAnything()
})

Events and State

The TestsRunner extends BaseRunner and emits various events during test execution. You can listen to these events to implement custom reporting, logging, or monitoring.

Events

Inherited from BaseRunner

  • preparing - Emitted when the test runner starts preparing
  • prepared - Emitted when preparation is complete
  • running - Emitted when tests start executing
  • releasing - Emitted when cleanup starts
  • released - Emitted when cleanup is complete
  • succeeded - Emitted when all tests pass
  • failed - Emitted when tests fail
  • stopped - Emitted when execution is stopped
  • timed-out - Emitted when execution times out

TestsRunner Specific Events

  • describe - Emitted when a describe block is registered

    testsRunner.on('describe', (event) => {
      console.log(`Describe: ${event.payload.name}`)
      console.log(`Options:`, event.payload.options)
    })
    
  • test - Emitted when a test is registered

    testsRunner.on('test', (event) => {
      console.log(`Test: ${event.payload.name}`)
      console.log(`Test object:`, event.payload.test)
    })
    

Test Lifecycle Events

Each individual test emits lifecycle events:

  • test:preparing - Individual test is preparing
  • test:prepared - Individual test preparation complete
  • test:running - Individual test is executing
  • test:releasing - Individual test cleanup started
  • test:released - Individual test cleanup complete
  • test:succeeded - Individual test passed
  • test:failed - Individual test failed
  • test:skipped - Individual test was skipped
  • test:error - Individual test had an error
  • test:stopped - Individual test was stopped
  • test:timed-out - Individual test timed out
testsRunner.on('test:succeeded', (event) => {
  console.log(`✅ ${event.payload.test.name} passed`)
})

testsRunner.on('test:failed', (event) => {
  console.log(`❌ ${event.payload.test.name} failed: ${event.payload.reason}`)
})

State

The TestsRunner provides a comprehensive state object that includes information about the test structure and execution status:

const state = testsRunner.state
console.log(state)

State Structure

interface TestsRunnerState {
  status: Status // Current runner status
  identifier: string // Runner identifier
  nodes: NodeState // Hierarchical test structure
  tests: TestRunnerState[] // Flat list of all tests
}

interface NodeState {
  status: Status // Node status (idle, running, succeeded, failed, etc.)
  name: string | symbol // Node name
  options: DescribeOptions // Options for this describe block
  tests: TestRunnerState[] // Tests directly in this node
  children: NodeState[] // Child describe blocks
}

interface TestRunnerState {
  status: Status // Test status
  id: string // Unique test ID
  name: string // Test name
  spec: string[] // Full test path (describe hierarchy + test name)
}

Example Usage

// Monitor test progress
testsRunner.on('**' as any, (event) => {
  const state = testsRunner.state
  const totalTests = state.tests.length
  const completedTests = state.tests.filter((t) => t.status === 'succeeded' || t.status === 'failed' || t.status === 'skipped').length

  console.log(`Progress: ${completedTests}/${totalTests} tests completed`)
})

// Get final results
await testsRunner.run()
const finalState = testsRunner.state
console.log(`Final status: ${finalState.status}`)
console.log(`Tests run: ${finalState.tests.length}`)

// Analyze test hierarchy
function printNodeStructure(node: NodeState, indent = 0) {
  const spaces = '  '.repeat(indent)
  console.log(`${spaces}${String(node.name)}: ${node.status}`)

  node.tests.forEach((test) => {
    console.log(`${spaces}  - ${test.name}: ${test.status}`)
  })

  node.children.forEach((child) => {
    printNodeStructure(child, indent + 1)
  })
}

printNodeStructure(testsRunner.state.nodes)

Typescript

This library is developed in TypeScript and shipped fully typed.

Contributing

The development of this library happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving this library.

License

MIT licensed.