Skip to content
GitHub

MeoCordTestingModule

class in meocord/testing Since 4.0.0

class MeoCordTestingModule

Builds testing modules: the classes a test needs, in a container of their own, with no Discord connection.

Use it for a controller, or anything MeoCord resolves for you: a service with injected dependencies, a guard, an interceptor, a presenter. A service that takes plain values needs no module; build it with new.

Examples

TypeScript
import { expect } from 'vitest'

@Controller()
class PingController {
  @Command('ping', CommandType.SLASH)
  async ping(interaction: ChatInputCommandInteraction) {
    await respond(interaction).send('pong')
  }
}
const module = MeoCordTestingModule.create({ controllers: [PingController] }).compile()
const interaction = createMockInteraction(ChatInputCommandInteraction, { commandName: 'ping' })
await module.invoke(PingController, 'ping', interaction)
expect(getResponse(interaction).calls[0].method).toBe('reply')

Members

constructor

new MeoCordTestingModule()

create

static create(options: TestingModuleOptions): TestingModuleBuilder

Starts building a testing module from the classes a test needs.

Parameters

NameTypeSinceDescription
optionsTestingModuleOptions

The controllers, providers, observers and app the module is built from.

options.controllers?(new (...args: any[]) => any)[]

The controllers the module builds, with every class they inject.

options.providers?Provider[]

Providers for what those classes inject, in any shape @MeoCord({ providers }) takes.

options.app?new (...args: any[]) => unknown4.1.0

The @MeoCord app class, whose global guards, interceptors and filters run with each handler's own, and whose translator, presenter, message options, theme, cooldown store and policy, and observers the module uses. A CooldownStore in providers takes the store's place. Its controllers, services and providers are not registered: list the ones a test needs, or build the whole app with MeoCordTestingModule.fromApp.

options.observers?(new (...args: any[]) => DispatchObserver)[]4.1.0

@Observer classes told about each call invoke, dispatch and emit make, after the app's own. The module waits for them before a call resolves, so a test sees what they were told.

options.shutdownTimeout?number4.1.0

How long close() waits, in milliseconds, for the calls under way, the cooldown store's operations and the onShutdown hooks, as shutdownTimeout in meocord.config.ts bounds the bot's shutdown: from 0 to 2147478647, and 10000 unless set. A test whose fake store never answers, or whose onShutdown never settles, sets it short.

Returns

TestingModuleBuilder

The builder, to override what the test replaces before compile().

fromApp Since 4.1.0

static fromApp(
  app: new (...args: any[]) => unknown,
  options?: FromAppOptions,
): TestingModuleBuilder

Starts building a testing module from a whole @MeoCord app, wired as the bot wires it.

Use it to test the app as it runs: every controller, service, provider and the cooldown store come from its @MeoCord({...}), with its stages, translator, presenter, message options, theme and observers, as TestingModuleOptions.app gives them. A test replaces what it must, by token, before anything is made.

Parameters

NameTypeDescription
appnew (...args: any[]) => unknown

The class @MeoCord decorates.

options?FromAppOptions

Providers that replace the app's by token, extra controllers, and observers.

options.providers?Provider[]

Providers that replace the app's own by token, or add what it lacks, such as the Discord Client.

options.controllers?(new (...args: any[]) => any)[]

Controllers built beside the app's, such as one only a test uses.

options.observers?(new (...args: any[]) => DispatchObserver)[]

@Observer classes told about each call, after the app's own.

options.shutdownTimeout?number

How long close() waits for the onShutdown hooks; see TestingModuleOptions.shutdownTimeout.

Returns

TestingModuleBuilder

The builder, whose override* methods still apply, for compile().

Throws

  • TypeError when app is not a @MeoCord class.

Examples

TypeScript
import { expect } from 'vitest'

const DATABASE = createToken<{ query(sql: string): Promise<unknown[]> }>('Database')
@Service()
class Notes {
  constructor(@Inject(DATABASE) private readonly db: { query(sql: string): Promise<unknown[]> }) {}
  list() {
    return this.db.query('select * from notes')
  }
}
@Controller()
class NotesController {
  constructor(private readonly notes: Notes) {}
  @Command('notes', CommandType.SLASH)
  async show(interaction: ChatInputCommandInteraction) {
    await respond(interaction).send(`${(await this.notes.list()).length} notes`)
  }
}
@MeoCord({
  controllers: [NotesController],
  providers: [{ provide: DATABASE, useFactory: async () => ({ query: async () => [] }) }],
  clientOptions: { intents: [] },
})
class App {}

// The app's own wiring, with an in-memory database in place of the real one
const module = await MeoCordTestingModule.fromApp(App, {
  providers: [{ provide: DATABASE, useValue: { query: async () => [{ id: 1 }] } }],
})
  .compile()
  .init()
const interaction = createMockInteraction(ChatInputCommandInteraction, { commandName: 'notes' })
await module.dispatch(interaction)
expect(getResponse(interaction).calls[0].payload).toMatchObject({ content: '1 notes' })

See also