Skip to main content
Orionjs uses Vitest for testing. This guide covers setup, conventions, and best practices.

Installation

bun add --dev vitest @vitest/coverage-v8
For MongoDB testing:
bun add --dev mongodb-memory-server

Configuration

Create a vitest.config.ts file in your project root:
import {defineConfig} from 'vitest/config'
import path from 'path'

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['app/**/*.test.ts', 'app/**/*.spec.ts'],
    setupFiles: ['./app/config/tests/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
    },
  },
  resolve: {
    alias: {
      app: path.resolve(__dirname, './app'),
    },
  },
})

Setup File

Create a ./app/config/tests/setup.ts file:
import {connections, createIndexesPromises, getMongoConnection} from '@orion-js/mongodb'
import {MongoMemoryServer} from 'mongodb-memory-server'

let mongod: MongoMemoryServer

beforeAll(async () => {
  mongod = await MongoMemoryServer.create()
  const uri = mongod.getUri()
  process.env.MONGO_URL = uri

  const connection = getMongoConnection({name: 'main'})
  await connection.connectionPromise
})

afterAll(async () => {
  await Promise.all(createIndexesPromises)

  for (const connectionName in connections) {
    const connection = connections[connectionName]
    await connection.client.close()
  }

  if (mongod) {
    await mongod.stop()
  }
})

Test Conventions

Arrange-Act-Assert Pattern

Follow the AAA pattern for all tests:
describe('CreateUserService', () => {
  it('should create a user with valid data', async () => {
    // Arrange - Set up test data and mocks
    const inputUserData = {
      name: 'John Doe',
      email: 'john@example.com'
    }
    const mockUsersRepo = {
      createUser: vi.fn().mockResolvedValue({_id: 'usr-123', ...inputUserData})
    }

    // Act - Execute the code under test
    const service = new CreateUserService()
    const actualUser = await service.execute(inputUserData)

    // Assert - Verify the results
    const expectedUser = {_id: 'usr-123', name: 'John Doe', email: 'john@example.com'}
    expect(actualUser).toEqual(expectedUser)
  })
})

Variable Naming Convention

Use clear, consistent variable names:
  • inputX - Input data for the test
  • mockX - Mocked dependencies
  • actualX - The actual result from the code under test
  • expectedX - The expected result to compare against
it('should calculate total price', () => {
  // Input
  const inputItems = [{price: 10}, {price: 20}]

  // Mock
  const mockDiscountService = {getDiscount: () => 0.1}

  // Act
  const actualTotal = calculateTotal(inputItems, mockDiscountService)

  // Assert
  const expectedTotal = 27 // 30 - 10% discount
  expect(actualTotal).toBe(expectedTotal)
})

Unit Tests

Write unit tests for each service function using test doubles to simulate dependencies:
import {mockService} from '@orion-js/services'
import {CreateCardService} from './CreateCardService'
import {CardsRepo} from '../repos/CardsRepo'
import {NotificationsRepo} from '../repos/NotificationsRepo'

describe('CreateCardService', () => {
  it('should create a card and send notification', async () => {
    // Arrange
    const inputCardData = {name: 'Test Card', status: 'draft'}
    const inputUserId = 'usr-123'

    mockService(CardsRepo, {
      createCard: vi.fn().mockResolvedValue({_id: 'crd-456', ...inputCardData})
    })

    mockService(NotificationsRepo, {
      createNotification: vi.fn().mockResolvedValue({_id: 'ntf-789'})
    })

    // Act
    const service = new CreateCardService()
    const actualCard = await service.execute(inputCardData, inputUserId)

    // Assert
    expect(actualCard._id).toBe('crd-456')
    expect(actualCard.name).toBe('Test Card')
  })
})

Acceptance Tests

Write acceptance tests for each module following the Given-When-Then convention:
describe('User Registration Flow', () => {
  it('should complete full registration process', async () => {
    // Given - A new user with valid registration data
    const inputRegistrationData = {
      email: 'newuser@example.com',
      password: 'SecurePass123',
      name: 'New User'
    }

    // When - The user submits registration
    const registrationService = new RegisterUserService()
    const actualResult = await registrationService.execute(inputRegistrationData)

    // Then - The user should be created and receive a welcome email
    expect(actualResult.user).toBeDefined()
    expect(actualResult.user.email).toBe('newuser@example.com')
    expect(actualResult.emailSent).toBe(true)
  })
})

Mocking Services

Use mockService from @orion-js/services to mock dependencies:
import {mockService} from '@orion-js/services'
import {UsersRepo} from '../repos/UsersRepo'
import {SecurityService} from './SecurityService'

describe('AuthenticateUserService', () => {
  beforeEach(() => {
    // Reset mocks before each test
    vi.clearAllMocks()
  })

  it('should authenticate valid users', async () => {
    // Mock the repository
    mockService(UsersRepo, {
      findByEmail: vi.fn().mockResolvedValue({
        _id: 'usr-123',
        email: 'test@example.com',
        password: 'hashed_password',
        salt: 'salt123',
        failedLoginAttempts: 0,
        roles: ['user']
      }),
      resetFailedLoginAttempts: vi.fn().mockResolvedValue(true)
    })

    // Mock services
    mockService(SecurityService, {
      verifyPassword: vi.fn().mockResolvedValue(true),
      calculateEffectivePermissions: vi.fn().mockResolvedValue(['read:profile'])
    })

    const authService = new AuthenticateUserService()
    const actualResult = await authService.execute('test@example.com', 'password')

    expect(actualResult.token).toBeDefined()
  })
})

Best Practices

  1. Use test doubles for dependencies - Except for third-party dependencies that are not expensive to execute
  2. Keep tests isolated - Each test should be independent and not rely on state from other tests
  3. Test behavior, not implementation - Focus on what the code does, not how it does it
  4. Use descriptive test names - Test names should describe the scenario being tested
  5. Don’t mock data for dev or prod - Mocking data is only for tests

Running Tests

# Run all tests
bunx vitest

# Run tests in watch mode
bunx vitest --watch

# Run tests with coverage
bunx vitest --coverage

# Run specific test file
bunx vitest app/users/services/CreateUser/index.test.ts