Skip to content
GitHub

TestingModuleBuilder

class in meocord/testing Since 4.0.0

class TestingModuleBuilder

Builds a testing module, with stand-ins for the providers, stages and theme a test replaces.

Use its override* methods before compile() to replace what a handler depends on, then run the handler with the TestingModule it returns.

Examples

TypeScript
@Controller()
class ProfileController {
  constructor(private readonly profiles: ProfileService) {}
  @Command('profile', CommandType.SLASH)
  @UseGuard(StaffGuard)
  async profile(interaction: ChatInputCommandInteraction) {
    await respond(interaction).send({ embeds: [await this.profiles.render(interaction.user.id)] })
  }
}
const module = MeoCordTestingModule.create({ controllers: [ProfileController] })
  .overrideProvider(ProfileService).useValue({ render: async () => new EmbedBuilder().setTitle('Ada') })
  .overrideGuard(StaffGuard).useValue({ canActivate: () => true })
  .compile()

Members

constructor

new TestingModuleBuilder(
  options: TestingModuleOptions,
  wiring?: AppWiring | undefined,
)

Parameters

NameTypeSinceDescription
optionsTestingModuleOptions
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.

wiring?AppWiring | undefined4.1.0

compile

compile(): TestingModule

Binds the controllers, providers and overrides into a module ready to resolve and run handlers.

It runs the bot's startup checks on commands and autocomplete handlers, except a command no builder registers, since a handler declared with a CommandType and no builder is how a test fixture is written. A subcommand path its command's builder does not register, a customId pattern given as a command name, a builder that registers another name, and component patterns that can match the same customId are named, as the bot names them.

Each check reports every startup error it finds, and the first is thrown, unchanged; when there are several, each is logged first, with the file it comes from. After reportAllStartupErrors, the errors decorators kept on the classes the module runs are among them.

Returns

TestingModule

The compiled module.

Throws

  • Error for the first startup error the checks find.

overrideFilter Since 4.1.0

overrideFilter(filter: new (...args: any[]) => ExceptionFilter<any>): {
  useValue: (stub: Partial<ExceptionFilter<any>>) => TestingModuleBuilder
}

Replaces an exception filter with a stub wherever it applies. The filter's @Catch still decides which errors reach the stub.

Parameters

NameTypeDescription
filternew (...args: any[]) => ExceptionFilter<any>

The filter class to replace.

Returns

{ useValue: (stub: Partial<ExceptionFilter<any>>) => TestingModuleBuilder }

Examples

TypeScript
builder.overrideFilter(RateLimitedFilter).useValue({ catch: vi.fn() })

overrideGuard

overrideGuard(guard: new (...args: any[]) => GuardInterface): {
  useValue: (stub: Partial<GuardInterface>) => TestingModuleBuilder
}

Replaces a guard with a stub wherever it applies, globally or on a controller or handler.

Parameters

NameTypeDescription
guardnew (...args: any[]) => GuardInterface

The guard class to replace.

Returns

{ useValue: (stub: Partial<GuardInterface>) => TestingModuleBuilder }

Examples

TypeScript
builder.overrideGuard(RateLimitGuard).useValue({ canActivate: () => true })

overrideInterceptor Since 4.1.0

overrideInterceptor(
  interceptor: new (...args: any[]) => InterceptorInterface,
): { useValue: (stub: Partial<InterceptorInterface>) => TestingModuleBuilder }

Replaces an interceptor with a stub wherever it applies, globally or on a controller or handler. The stub's intercept receives the context and next; call next.handle() to run the handler.

Parameters

NameTypeDescription
interceptornew (...args: any[]) => InterceptorInterface

The interceptor class to replace.

Returns

{ useValue: (stub: Partial<InterceptorInterface>) => TestingModuleBuilder }

Examples

TypeScript
builder.overrideInterceptor(TimingInterceptor).useValue({ intercept: (_context, next) => next.handle() })

overrideProvider

overrideProvider<T>(token: ProviderToken<T> | ServiceIdentifier<T>): {
  useValue: (value: Partial<T>) => TestingModuleBuilder
}

Replaces a provider with a test double.

The double needs only the members the test uses; misspelled member names are still rejected.

Parameters

NameTypeDescription
tokenProviderToken<T> | ServiceIdentifier<T>

The provider to replace.

Returns

{ useValue: (value: Partial<T>) => TestingModuleBuilder }

Examples

TypeScript
builder.overrideProvider(UserService).useValue({ findUser: vi.fn() })

overrideTheme Since 4.1.0

overrideTheme(theme: ThemeOverride): TestingModuleBuilder

Changes part of the app's @MeoCord({ theme }) for this module, or gives a module without an app a theme. It goes over the app's theme, so it names only the tokens it changes; each @UseTheme, and what themeFor looks up, still goes over it.

Parameters

NameTypeDescription
themeThemeOverride

The tokens to change, checked as @MeoCord({ theme }) checks them, and copied.

theme.colors?{ primary?: ColorResolvable neutral?: ColorResolvable success?: ColorResolvable warning?: ColorResolvable danger?: ColorResolvable info?: ColorResolvable }

Colours by role.

theme.colors.primary?ColorResolvable

The app's own colour, for what is neither good nor bad news.

theme.colors.neutral?ColorResolvable

A quiet colour, for what needs no attention.

theme.colors.success?ColorResolvable

Something went as asked.

theme.colors.warning?ColorResolvable

Something needs the user's attention, or was refused because of what they did.

theme.colors.danger?ColorResolvable

Something failed, a fault in the bot rather than the user.

theme.colors.info?ColorResolvable

Information, with no action needed.

theme.emojis?{ loading?: string success?: string warning?: string danger?: string info?: string }

Emojis by role.

theme.emojis.loading?string

Shown while a deferred call is still working.

theme.emojis.success?string

Something went as asked.

theme.emojis.warning?string

Something needs the user's attention, or was refused because of what they did.

theme.emojis.danger?string

Something failed, a fault in the bot rather than the user.

theme.emojis.info?string

Information, with no action needed.

theme.buttons?{ primary?: ThemeButtonStyle neutral?: ThemeButtonStyle success?: ThemeButtonStyle danger?: ThemeButtonStyle }

Button styles by role.

theme.buttons.primary?ThemeButtonStyle

The action the app most expects, such as Submit.

theme.buttons.neutral?ThemeButtonStyle

A secondary action, such as Cancel or Back.

theme.buttons.success?ThemeButtonStyle

An action that confirms or approves, such as Approve.

theme.buttons.danger?ThemeButtonStyle

An action that removes or refuses, such as Delete or Reject.

Returns

TestingModuleBuilder

Throws

  • Error naming each token that is not valid.

Examples

TypeScript
const module = MeoCordTestingModule.create({ app: App, controllers: [ShopController] })
  .overrideTheme({ colors: { primary: '#E3606D' } })
  .compile()

overrideThemeFor Since 4.1.0

overrideThemeFor(
  resolvers:
    ThemeResolvers | (new (...args: any[]) => ThemeResolver) | undefined,
): TestingModuleBuilder

Replaces the app's @MeoCord({ themeFor }) for this module, or removes it with undefined. The app's themeCache and themeForTimeoutMs still apply, and the results are cached in TestingModule.themeCache, as the bot caches them. A mock resolver shows each lookup. A class implementing ThemeResolver is bound as the app binds one, so overrideProvider replaces it or what it injects.

Parameters

NameTypeDescription
resolvers| ThemeResolvers | (new (...args: any[]) => ThemeResolver) | undefined

The server's and the user's resolvers, a class implementing ThemeResolver, or undefined for none.

Returns

TestingModuleBuilder

Throws

  • TypeError when a resolver is not a function, or is neither guild nor user, or a class has neither method.

Examples

TypeScript
const guild = vi.fn(() => ({ colors: { primary: '#26A042' as const } }))
const module = MeoCordTestingModule.create({ app: App, controllers: [ShopController] }).overrideThemeFor({ guild }).compile()

See also