TestingModule
class in meocord/testing Since 4.0.0
class TestingModuleA compiled testing module, which runs handlers as the bot does and resolves the classes it built.
Use invoke to run a handler you name, dispatch to send an input through the bot's routing, and emit for a
gateway event. getResponse then reports what a handler sent.
Examples
import { expect } from 'vitest'
@Controller()
class TicketController {
@Command('ticket/{id}/close', CommandType.BUTTON)
async close(interaction: ButtonInteraction, { id }: { id: string }) {
await respond(interaction).send(`Ticket ${id} closed.`)
}
}
const module = MeoCordTestingModule.create({ controllers: [TicketController] }).compile()
const { handlers } = await module.dispatch(createMockInteraction(ButtonInteraction, { customId: 'ticket/7/close' }))
expect(handlers).toEqual([{ controller: TicketController, method: 'close', ran: true }])Members
constructor
new TestingModule(
container: Container,
controllers?: readonly (new (...args: any[]) => unknown)[],
eventClasses?: readonly (new (...args: any[]) => unknown)[],
providers?: ProviderMap,
order?: readonly unknown[],
messageOptions?: MessageCommandOptions,
lifecycle?: readonly LifecycleUnit[],
constructed?: ReadonlySet<unknown>,
appWarnUnanswered?: boolean | undefined,
services?: readonly (new (...args: any[]) => unknown)[],
shutdownTimeout?: number,
)Parameters
| Name | Type | Default | Since | Description |
|---|---|---|---|---|
container | Container | |||
controllers? | readonly (new (...args: any[]) => unknown)[] | 4.1.0 | ||
eventClasses? | readonly (new (...args: any[]) => unknown)[] | 4.1.0 | ||
providers? | ProviderMap | 4.1.0 | ||
order? | readonly unknown[] | 4.1.0 | ||
messageOptions? | MessageCommandOptions | 4.1.0 | ||
messageOptions.prefix? | | MessagePrefix
| ((
message: Message,
) =>
| MessagePrefix
| null
| undefined
| Promise<
MessagePrefix | null | undefined
>) | 4.1.0 | What a message starts with to reach a patterned handler: a prefix, a list of them, or a function
of the message returning them, such as a server's own prefix. Without one, a pattern matches the
message as it is. Where a function finds none, returning an empty list, | |
messageOptions.mention? | boolean | 'only' | false | 4.1.0 | Also accepts a mention of the bot, |
messageOptions.caseSensitive? | boolean | false | 4.1.0 | Matches the prefix, a pattern's literal words, its choice words and its flag names in the case written. A text param keeps the case the user typed either way. |
messageOptions.types? | Record<string, MessageParamType> | 4.1.0 | Param types of the app's own, used in patterns as | |
messageOptions.deleteUsageRepliesAfter? | number | 10 | 4.1.0 | How long a reply showing a command's usage, or a guard's or validation's reason, stays before it is deleted, in
seconds. |
messageOptions.replyEmoji? | boolean | false | 4.1.0 | Begins every text reply MeoCord sends to a message with the theme's |
messageOptions.dmOnError? | boolean | false | 4.1.0 | Tells the author of a message command, in a direct message, when it fails with an error no filter handled,
naming the command, the channel and the server. The error is logged as without it, and nothing is said in the
channel: a message cannot be answered privately there. A command sent in a direct message is answered in it.
The text is |
messageOptions.dmOnCooldown? | boolean | false | 4.1.0 | Tells the author of a message command, in a direct message, when a |
messageOptions.help? | boolean | MessageHelpOptions | false | 4.1.0 | Answers |
lifecycle? | readonly LifecycleUnit[] | 4.1.0 | ||
constructed? | ReadonlySet<unknown> | 4.1.0 | The lifecycle units the container has constructed, which | |
appWarnUnanswered? | boolean | undefined | 4.1.0 | The | |
services? | readonly (new (...args: any[]) => unknown)[] | 4.1.0 | The app's listed services, made at | |
shutdownTimeout? | number | 4.1.0 | How long |
themeCache Since 4.1.0
get themeCache(): ThemeCacheThe module's ThemeCache: the instance its classes inject, holding what themeFor, or
overrideThemeFor, looked up in this module's calls. Clear a result to have the next call look it up
again. Each module has its own.
close Since 4.1.0
close(): Promise<void>Runs the onShutdown hooks of every class and provided value the module has constructed, once, as
the bot does when it stops: one at a time, in reverse, so a class stops before the classes and
providers it uses. A factory's value, such as a connection pool init() made, is closed after
everything that injects it, whether or not init({ ready: true }) ran. Nothing is constructed
just to be shut down. Every hook runs even when one fails. Calling it again does nothing more.
It first gives up the theme read outside calls, if init({ ready: true }) made it this module's;
reads outside calls then return MeoCord's defaults until another module or app is ready.
The cooldown store and what it injects shut down last, in the same sequence as the bot's. When any of them has an
onShutdown, the calls invoke, dispatch and emit have under way finish first, then the store operations
they started, so the store's last writes still reach it. The module stops waiting for the whole sequence after
its shutdownTimeout, 10 seconds unless set, and logs that it did. Give a test whose fake store never answers, or
whose onShutdown never settles, a short shutdownTimeout.
Returns
Promise<void>Once every hook has run. Rejects with the error of a hook that failed, or an
AggregateError naming each when several did.
Examples
const module = await MeoCordTestingModule.create({ providers: [{ provide: POOL, useFactory: createPool }] })
.compile()
.init()
await module.close()
expect(module.get(POOL).ended).toBe(true)dispatch Since 4.1.0
dispatch(input: Interaction | Message): Promise<DispatchedCall>
dispatch(
reaction: MessageReaction | PartialMessageReaction,
options: { user: User | PartialUser; action?: ReactionHandlerAction },
): Promise<DispatchedCall>Sends an interaction, a message or a reaction through the bot's own dispatch: routed over the module's
controllers and its app's message options exactly as the bot routes it, then run through the full
pipeline of each handler it reaches. What the user is sent is sent to the mock, as the bot sends it:
the handler's answer, a usage reply, or the built-in fallback's answer to an error no filter handles.
Inputs the bot skips, such as a message from a bot, reach nothing. The module waits for its observers.
dispatch tests what the bot does with an input: which handler it reaches, with what params, and what
the user sees. To test one handler you name, whatever would route to it, use invoke.
Parameters
| Name | Type | Description |
|---|---|---|
input | Interaction | Message | An interaction or a message; or a reaction, with the user who reacted and whether they
added it, |
Returns
Promise<DispatchedCall>Every handler reached, in the order it ran, whether any ran, and the first error a call ended
with. An error the fallback answers as the user's own outcome resolves: a usage reply, an unknown
command, or a guard's, a cooldown's, a validation's or a UserError's refusal, as the fallback
answers each for an interaction or a message. Any other error no filter handles rejects the call once
the fallback has answered and every handler has run: with that error, or an AggregateError when
several were left unhandled.
Examples
const module = MeoCordTestingModule.create({ app: App, controllers: [CardController] }).compile()
const interaction = createMockInteraction(ButtonInteraction, { customId: 'card/summary/7' })
const { handlers } = await module.dispatch(interaction)
expect(handlers).toEqual([{ controller: CardController, method: 'summary', ran: true }])Parameters
| Name | Type | Description |
|---|---|---|
reaction | MessageReaction | PartialMessageReaction | |
options | {
user: User | PartialUser
action?: ReactionHandlerAction
} | |
options.user | User | PartialUser | |
options.action? | ReactionHandlerAction |
Returns
Promise<DispatchedCall>Every handler reached, in the order it ran, whether any ran, and the first error a call ended
with. An error the fallback answers as the user's own outcome resolves: a usage reply, an unknown
command, or a guard's, a cooldown's, a validation's or a UserError's refusal, as the fallback
answers each for an interaction or a message. Any other error no filter handles rejects the call once
the fallback has answered and every handler has run: with that error, or an AggregateError when
several were left unhandled.
Examples
const module = MeoCordTestingModule.create({ app: App, controllers: [CardController] }).compile()
const interaction = createMockInteraction(ButtonInteraction, { customId: 'card/summary/7' })
const { handlers } = await module.dispatch(interaction)
expect(handlers).toEqual([{ controller: CardController, method: 'summary', ran: true }])emit Since 4.1.0
emit<E extends keyof ClientEvents>(
event: E,
...args: ClientEvents[E]
): Promise<EmitResult>Emits a client event to the module's @On and @Once handlers, through the same pipeline the app
runs them in: the global guards of the module's app, then each handler's own. Handlers on the
module's controllers, class providers and their dependencies all receive it. A @Once handler
handles only the first event, as it would on a client.
Parameters
| Name | Type | Description |
|---|---|---|
event | E | The client event, such as |
...args | ClientEvents[E] | The event's arguments, typed from discord.js's |
Returns
Promise<EmitResult>How many handlers ran. Rejects once every handler has settled if any threw: with that
error when one handler failed, or an AggregateError of them when several did.
Examples
const module = MeoCordTestingModule.create({ controllers: [WelcomeController] }).compile()
const member = createMock<GuildMember>()
const { ran } = await module.emit('guildMemberAdd', member)
expect(ran).toBe(1)get
get<T>(token: ProviderToken<T> | ServiceIdentifier<T>): TResolves an instance from the module, as the bot would inject it.
Parameters
| Name | Type | Description |
|---|---|---|
token | ProviderToken<T> | ServiceIdentifier<T> | A controller, a provided token, or another bound class. |
Returns
TThe instance, with its dependencies and overrides applied.
Throws
When the instance depends on a factory that returns a promise and
init()has not run.
init Since 4.1.0
init(options?: TestingModuleInitOptions): Promise<this>Resolves the module's useFactory providers, awaiting those that return a promise, in dependency
order. invoke, dispatch and emit call it first; call it yourself before get resolves anything that
depends on an asynchronous factory. Calling it again does nothing more.
With { ready: true }, it then runs every onReady hook once, as the bot does once it is online:
one at a time, each class after the classes and providers it injects, the observers' last. Every
hook runs even when one fails. Pair it with close, which runs the onShutdown hooks.
Ready, the module's app theme is also the one useTheme() reads outside any call, as a bot's is once
it is online, unless another module or app in the process was ready first: that one keeps it, and
this module's theme reaches its own calls only. close gives it up; nothing takes it over.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
options? | TestingModuleInitOptions |
| |
options.ready? | | boolean
| {
client?: Client<true>
primary?: boolean
} | false | Also runs every |
Returns
Promise<this>The module, once every factory has made its value and every onReady hook asked for has
run. Rejects with the error of a factory or a hook that failed, or an AggregateError of the
hooks when several did.
Examples
const module = await MeoCordTestingModule.create({
controllers: [NotesController],
providers: [{ provide: DATABASE, useFactory: async () => createTestDatabase() }],
})
.compile()
.init({ ready: true })
expect(module.get(NotesStore).loaded).toBe(true)
await module.close()invoke Since 4.1.0
invoke<C extends new (...args: any[]) => unknown, M extends HandlerName<C>>(
controller: C,
methodName: M,
...args: HandlerArgs<C, M>
): Promise<InvocationResult>Runs a handler through the same pipeline dispatch runs: @Defer's acknowledgement, the global
guards of the module's app, then the handler's own, in order and once each; then the
interceptors, the app's first, around validation, pipes, cooldowns and the handler; all inside the
handler's exception filters. Guards resolve
from this module, so overrideGuard stubs apply and guards that inject ExecutionContext receive
it. overrideInterceptor and overrideFilter stubs apply the same way.
Calling the controller method directly runs its guards but no interceptors, validation or
filters; invoke is the way to test everything dispatch runs around a handler.
invoke tests one handler you name, and an error no filter handles rejects the call. That
includes the errors the bot answers the user with: a guard's GuardDeniedError, a UserError and
a CooldownError reject invoke, where dispatch resolves { ran, error } and sends the answer.
To test what the bot does with an input, which handler it reaches and what the user is sent, use
dispatch.
Parameters
| Name | Type | Description |
|---|---|---|
controller | C | A controller of the module: one given to |
methodName | M | The handler method's name. |
...args | HandlerArgs<C, M> | The arguments dispatch would pass: the interaction, message or reaction, then the
handler's params. With an interaction alone, the params are built as dispatch builds them: a
command's or an autocomplete's options, or the handler's customId params with a modal's fields or
a select menu's choices.
An interaction's customId or command name must be one dispatch routes to the handler, ranking
every handler of the module as the bot does; a mock built without one is not checked. With a message alone, a patterned |
Returns
Promise<InvocationResult>Whether the handler ran, and the error a filter handled, if any. Rejects with an error no filter handles, or with the error a filter throws: the built-in fallback, which answers such errors in the bot, does not run here. Rejects before running anything with an interaction or a message the handler's route does not match, naming both.
Examples
const module = MeoCordTestingModule.create({ controllers: [ModerationController] }).compile()
const interaction = createMockInteraction(ChatInputCommandInteraction)
// A guard that returns false
const { ran } = await module.invoke(ModerationController, 'ban', interaction)
expect(ran).toBe(false)
expect(interaction.reply).not.toHaveBeenCalled()
// A guard that throws GuardDeniedError, or a handler that throws UserError
await expect(module.invoke(ModerationController, 'kick', interaction)).rejects.toThrow(GuardDeniedError)
// dispatch answers it as the bot does, and resolves with the outcome
const outcome = await module.dispatch(createMockInteraction(ChatInputCommandInteraction, { commandName: 'kick' }))
expect(outcome.ran).toBe(false)
expect(outcome.error).toBeInstanceOf(GuardDeniedError)See also
- MeoCordTestingModule
- getResponse
- Invoke and dispatch