Tests Without a Token

A real client against an in-memory Fluxer, so tests run without a token or network access

A lab flask with a large checkmark on it

One Set of Options for the Bot and Its Tests

The testing entry point, @neontechspace/fluxerly/testing, ships with the SDK. Its createTestBot takes the same options object as runBot, except instance and transport, which the test client owns

Keep that object in a module of its own, so the bot and its tests run the same events and commands. The token can stay unset in a test

Share Options with Tests
bot-options.ts
import type { BotOptions } from "@neontechspace/fluxerly"// The bot's runBot options, shared by bot.ts and the testsexport const botOptions: BotOptions = {    token: process.env.FLUXER_BOT_TOKEN,    commands: {        prefix: "!",        commands: {            ping: {                description: "Check that the bot replies",                execute: ({ reply }) => reply("Pong!"),            },        },    },}

A First Test with Say

The test client is a real client connected to an in-memory Fluxer. say sends a message as a fake user, waits until the bot has finished and returns what it sent

The message goes through the SDK's own decoding and handlers, as a real one would. node:test is the runner built into Node.js, so there is nothing else to install

The First Test in the Testing Guide
bot.test.ts
import assert from "node:assert/strict"import { test } from "node:test"import { createTestBot } from "@neontechspace/fluxerly/testing"import { botOptions } from "./bot-options.ts"test("!ping replies Pong!", async () => {    await using bot = createTestBot(botOptions)    await bot.ready()    const replies = await bot.say("!ping")    assert.deepEqual(        replies.map((message) => message.content),        ["Pong!"],    )})
node --test

A Broken Handler Fails the Test

A command that throws is recorded as commands.failed, and failures() returns it so the test can expect it

A failure that no test reads makes the shutdown reject. A broken handler fails the test even when no assertion looks at it

What the Test Does
A beacon that lights on failure
bot.test.ts
test("a command that throws is reported", async () => {    await using bot = createTestBot({        commands: {            prefix: "!",            commands: {                broken: {                    execute: () => {                        throw new Error("Not implemented yet")                    },                },            },        },    })    await bot.ready()    bot.emit("MESSAGE_CREATE", bot.fixtures.message({ content: "!broken" }))    await bot.idle()    assert.deepEqual(        bot.failures().map((record) => record.code),        ["commands.failed"],    )})

Answer as the API Would

rest.respond tells the in-memory Fluxer how to answer a request, and returns a route whose next() waits for the request it answered

Code that registers handlers with client.on instead of runBot is tested with createTestClient, the same test client without bot options

Test client.on Handlers
TypeScript
import assert from "node:assert/strict"import { test } from "node:test"import type { Client } from "@neontechspace/fluxerly"import { createTestClient } from "@neontechspace/fluxerly/testing"// The application passes its own client and welcome channelexport function installWelcome(client: Client, channelId: string) {    return client.on("guildMemberAdd", (member) => client.messages.send(channelId, `Welcome, ${member.userId}!`))}test("new members are welcomed", async () => {    await using bot = createTestClient()    const sent = bot.rest.respond("POST /channels/:id/messages", { body: bot.fixtures.message() })    installWelcome(bot.client, bot.fixtures.ids.channel)    await bot.ready()    bot.emit("GUILD_MEMBER_ADD", bot.fixtures.member())    const request = await sent.next()    assert.match(String((request.body as { content?: unknown }).content), /^Welcome/)})

The Same for Effect

Applications built with Effect, a TypeScript library for typed failures and scoped cleanup, test with @neontechspace/fluxerly/effect/testing

It has the same createTestBot. Its controls are Effects, and the test bot shuts down when the surrounding Effect.scoped ends

The Effect Application Testing Guide
TypeScript
import { Effect } from "effect"import { createTestBot } from "@neontechspace/fluxerly/effect/testing"export const pingTest = Effect.scoped(    Effect.gen(function* () {        const bot = yield* createTestBot({            token: process.env.FLUXER_BOT_TOKEN,            events: {                messageCreate: ({ message, reply }) =>                    message.content !== "!ping" ? Effect.void : reply("Pong!"),            },        })        yield* bot.ready()        const replies = yield* bot.say("!ping")        const ignored = yield* bot.say("hello")        return { replies: replies.map((message) => message.content), ignored: ignored.length }    }),)