Skip to content

Testing with Bun

Overview

All tests use Bun's built-in test runner. Never use Jest — imports and APIs differ.

Setup

typescript
import { describe, it, expect, mock, spyOn, beforeEach, afterEach } from 'bun:test'

File Naming

  • Unit tests: [ClassName].test.ts
  • Integration tests: [ClassName].integration.test.ts

Tests live in packages/<context>/test/ mirroring the source structure.

AAA Pattern (Arrange, Act, Assert)

Always follow this structure:

typescript
it('should save the coin blockchain', async () => {
  // Arrange
  const params = { coinId: 'BTC', blockchain: 'BTC' }
  const repository = { save: mock(() => Promise.resolve()) } as unknown as CoinBlockchainRepository

  // Act
  const service = new CoinBlockchainCreator(repository)
  await service.run(params)

  // Assert
  expect(repository.save).toHaveBeenCalled()
})

Mocking

Use mock() from bun:test. Cast with as unknown as InterfaceType:

typescript
import { mock } from 'bun:test'
import type { CexSpotAccountPort } from '../../../domain/ports/CexSpotAccountPort'

const cexAdapter: CexSpotAccountPort = {
  coinBlockchainList: mock(() => Promise.resolve([])),
  depositAddress: mock(() => Promise.resolve(null))
} as unknown as CexSpotAccountPort

Override mock return values per test:

typescript
it('should handle empty list', async () => {
  cexAdapter.coinBlockchainList = mock(() => Promise.resolve([]))
  await service.run({ exchange: 'binance' })
  expect(repository.save).not.toHaveBeenCalled()
})

Object Mother Pattern

Test builders live in packages/<context>/test/mothers/[Entity]Mother.ts. They provide sensible defaults with optional overrides:

typescript
// packages/cex-wallets/test/mothers/CexAccountMother.ts
import { CexAccount } from '../../domain/entities/CexAccount'
import { CEX_BINANCE_MAIN_ACCOUNT } from '../test-constants'

export class CexAccountMother {
  static create(
    params: {
      id?: string
      cex?: string
      active?: boolean
    } = {}
  ): CexAccount {
    return CexAccount.fromDto({ ...CEX_BINANCE_MAIN_ACCOUNT, ...params })
  }
}

Usage:

typescript
const account = CexAccountMother.create()
const inactiveAccount = CexAccountMother.create({ active: false })
const kuCoinAccount = CexAccountMother.create({ cex: 'kucoin' })

Test Constants

test-constants.ts holds reusable test data (sanitized, no real credentials):

typescript
// packages/cex-wallets/test/test-constants.ts
export const CEX_BINANCE_MAIN_ACCOUNT = {
  id: 'test-account-id',
  cex: 'binance',
  accountType: 'MAIN',
  active: true,
  encryptedApiKey: 'encrypted-test-key',
  encryptedApiSecret: 'encrypted-test-secret'
}

Complete Service Test Example

typescript
import { describe, it, expect, mock, beforeEach } from 'bun:test'
import { CoinBlockchainCreator } from '../../../application/services/CoinBlockchainCreator'
import type { CoinBlockchainRepository } from '../../../domain/repositories/CoinBlockchainRepository'
import type { EventPublisher } from '@sws/common/domain'

describe('CoinBlockchainCreator', () => {
  let repository: CoinBlockchainRepository
  let eventPublisher: EventPublisher
  let service: CoinBlockchainCreator

  beforeEach(() => {
    repository = {
      save: mock(() => Promise.resolve()),
      findById: mock(() => Promise.resolve(null))
    } as unknown as CoinBlockchainRepository

    eventPublisher = {
      publish: mock(() => Promise.resolve())
    } as unknown as EventPublisher

    service = new CoinBlockchainCreator(repository, eventPublisher)
  })

  it('should save a coin blockchain', async () => {
    await service.run({ coinId: 'BTC', blockchain: 'BTC' })
    expect(repository.save).toHaveBeenCalled()
  })

  it('should publish a domain event', async () => {
    await service.run({ coinId: 'BTC', blockchain: 'BTC' })
    expect(eventPublisher.publish).toHaveBeenCalled()
  })
})

Factory Test Overrides

Use the factory with TestDependencyOverrides to inject mocks:

typescript
import type { TestDependencyOverrides } from '@sws/common/test'
import { CexAccountFactory } from '../../infrastructure/factories/CexAccountFactory'

const mockRepository = {
  findById: mock(() => Promise.resolve(null)),
  save: mock(() => Promise.resolve())
} as unknown as CexAccountRepository

const overrides: TestDependencyOverrides = { cexAccountRepository: mockRepository }
const service = CexAccountFactory.coinBlockchainCreator(overrides)

Running Tests

bash
bun test                           # All tests
bun test packages/cex-wallets      # Specific package
bun test CoinBlockchainCreator     # Match by name
bun test --watch                   # Watch mode
TEST_LOGGER=1 bun test             # With logs enabled