Testing

Test your application with the built-in TestApp utility.

MiiaJS provides a TestApp class for writing unit and integration tests. Import it from @miiajs/core/testing.

TestApp API

import { TestApp } from '@miiajs/core/testing'

const app = await TestApp.create(AppModule)
  .override(UserService, mockUserService)
  .compile()

Builder methods

MethodDescription
TestApp.create(...modules)Create a test app from modules
.provide(...providers)Add additional providers
.override(token, value)Replace a provider with a mock
.use(...middlewares)Add middleware
.useGuard(...guards)Add guards
.compile()Build the test app (returns Promise<TestApp>)

Instance methods

MethodDescription
.request(method, path, options?)Send an HTTP request
.resolve(token)Resolve a provider from the container
.close()Clean up resources

Integration tests

Test your HTTP endpoints end-to-end:

import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { TestApp } from '@miiajs/core/testing'
import { AppModule } from '../src/app.module.js'

describe('UserController', () => {
  let app: TestApp

  beforeAll(async () => {
    app = await TestApp.create(AppModule)
      .override(UserService, {
        findAll: () => [{ id: 1, name: 'Alice' }],
        create: (data) => ({ id: 2, ...data }),
      })
      .compile()
  })

  afterAll(() => app.close())

  it('GET /users should return users', async () => {
    const res = await app.request('GET', '/users')

    expect(res.status).toBe(200)
    expect(await res.json()).toEqual([{ id: 1, name: 'Alice' }])
  })

  it('POST /users should create a user', async () => {
    const res = await app.request('POST', '/users', {
      body: { name: 'Bob', email: 'bob@test.com' },
    })

    expect(res.status).toBe(201)
    expect(await res.json()).toMatchObject({ name: 'Bob' })
  })

  it('POST /users should validate body', async () => {
    const res = await app.request('POST', '/users', {
      body: { invalid: true },
    })

    expect(res.status).toBe(422)
  })
})

Unit tests

Test individual services with mocked dependencies:

describe('UserService', () => {
  let app: TestApp
  let service: UserService

  beforeAll(async () => {
    app = await TestApp.create()
      .provide(UserService)
      .override(DatabaseService, {
        query: () => [{ id: 1, name: 'Alice' }],
      })
      .compile()

    service = app.resolve(UserService)
  })

  afterAll(() => app.close())

  it('should find all users', async () => {
    const result = await service.findAll()
    expect(result).toEqual([{ id: 1, name: 'Alice' }])
  })
})

Request options

The request() method accepts an options object:

const res = await app.request('POST', '/api/users', {
  body: { name: 'Alice' },
  headers: {
    'Authorization': 'Bearer my-token',
    'X-Custom': 'value',
  },
  query: {
    page: '1',
    limit: '10',
  },
})
OptionTypeDescription
bodyanyRequest body (auto-serialized to JSON)
headersRecord<string, string>Request headers
queryRecord<string, string>Query parameters

Testing with auth

import { JwtService } from '@miiajs/auth/jwt'

describe('Protected routes', () => {
  let app: TestApp
  let token: string

  beforeAll(async () => {
    app = await TestApp.create(AppModule)
      .override(UserService, mockUserService)
      .compile()

    const jwt = app.resolve(JwtService)
    token = await jwt.sign({ sub: '1', role: 'admin' })
  })

  it('should reject unauthenticated requests', async () => {
    const res = await app.request('GET', '/api/users/1')
    expect(res.status).toBe(401)
  })

  it('should allow authenticated requests', async () => {
    const res = await app.request('GET', '/api/users/1', {
      headers: { Authorization: `Bearer ${token}` },
    })
    expect(res.status).toBe(200)
  })
})