Why a Discord in memory
A test needs a Discord to run against, and the real one is a bad fit: it needs a token, a server, people clicking, and it shows every try to everyone. Sochapterjs test plays Discord itself: a server that lives in memory, with a #general channel, an owner, the bot as an admin, and the people you name. Your files run exactly as they would online, against the same framework; only the other end is pretend. Nothing to connect to, nothing to configure, and it runs in a second.
Run it
chapterjs dev would:
/dice should answer, only that it must answer without breaking. Most bugs are caught right there. What your files print meanwhile (the console.log of heartbeat, for one) stays out of the report, and is shown under a check that failed.
A scenario
For what the bot should say, you write a scenario: a file ending in.test.ts, anywhere in src/, that the bot never loads. Create src/commands/dice.test.ts:
src/commands/dice.test.ts
Line by line
scenario('…', async bot => { … })is a test: a label, shown in the report, and a function. A file may export several, in a list.botis your bot in its test server.bot.use(dice, { sides: 20 })uses/diceas a person would: the options are typed from the file, sosidesmust be a whole number andwhoa person. Hover them, your editor knows.- What comes back is the answer.
says('rolled a')checks that the text contains it,isPublic()that everyone sees it,hasButton('Roll again')that the button is there. Each check throws a plain sentence when it fails, and returns the answer otherwise, so they chain. bot.click(reply.button('Roll again'), { on: reply })presses the button of that answer.updatesMessage()checks that the bot changed the message instead of sending a new one: that is whatinteraction.update()does.bot.member('alice')is a person of the server, made the first time the name is given. Her display name isAlice, which/whoisshows.
When it fails
Change the scenario to expect'rolled an' and run again:
chapterjs dev reports an error in a command. A scenario also fails when a function of the bot throws while it runs, even if every check passed: a person would have seen “Something went wrong”, and that is never what you want.
Points of attention
- Who does things. When nobody is named,
bot.someonedoes: a person with no special rights. A command withpermissionsrefuses them, as Discord would. Name someone with what they need:bot.member('mod', { permissions: ['ModerateMembers'] }), then{ by: mod }as the last argument. - What Discord would refuse is refused before the bot. A required option left out, a number out of its range, a text too long: the scenario fails with the reason, because the Discord app would never have sent that.
- The server keeps what the bot did.
bot.channel('general').lastis the newest message there,member.bannedsays whether someone was banned,member.timedOutwhether they are timed out,channel.threadslists the threads. For anything else,node:assertworks as in any test. - Time does not pass. A
cooldown, or a task that runseveryhour, can not be tested by waiting.bot.run(task)runs a task now, as if its time had come. - Not everything is there yet. Voice, polls and scheduled events are not simulated: an event the server can not make happen is named in the report with
ℹ, and the rest is checked.
Recap
chapterjs testruns your bot against a Discord in memory: no token, no server, no one watching.- Without a line written, every command, component, event and task is used once with ordinary and hard values, and must answer without throwing.
- A scenario is
scenario('label', async bot => { … })in a*.test.tsfile ofsrc/:bot.use(),bot.click(),bot.say(),bot.join()…, and the answer says what the bot did. - A failure names the file and the line, like every other error.
Building
Check the whole bot and compile it for production.
Guide: Testing
The same, in short, with everything a scenario can do.