Adding tests to your Discord Bot - Discord Bot Series (Part 3)
Kevin Schildhorn
Posted on July 6, 2022
Discord is a great platform for keeping in touch with friends, and adding a bot can enrich that experience. A bot can respond to messages, perform an action based on a command, or even add music to a voice chat.
In this series we'll be going over how to create a small example bot by using the NPM module for DiscordJS in a KotlinJS project.
Note: This is the third post in a series of three posts, about how to use KotlinJS and NPM modules to create a full fledged project, in this case a Discord Bot.
Previously
In my last post we went over how to add DiscordJS to our project, and start creating our bot. We added a listener for messages and replied to "hello" with a response of the computer name. I mentioned at the end that we weren't catching some edge cases with our response, and so in this post we'll be adding tests to our discord bot.
As a refresher, in this series we're going to create a small discord bot that responds to a specific message with its computer name.
Creating tests in kotlin
This article assumes you understand some basics about adding tests in a kotlin project, specifically using the kotlin.test
library. You can see a helpful guide from jetbrains.
Adding testing dependencies
First off, we need to add dependencies to testing libraries. Make sure you have these dependencies in your build.gradle
file.
dependencies {
testImplementation(kotlin("test"))
testImplementation(kotlin("test-js"))
}
Testing our logic
In the last post we added logic to respond to "hello" messages from users in the channel. We now want to test and make sure our bot says hello only when it gets a correct message. We want to have them ignore capitalization and not respond to other bots (I mean they can if they want, this is just for the example). Feel free to add any other requirements you want!
Separating out code
As it is now, we cannot test our code as it's just sitting inside the "messageCreate" callback. First we should move our code to a separate file. Let's make a new class called HelloHandler
:
class HelloHandler {
fun handleMessage(message: Message){
if(message.content == "hello")
message.channel.send("...")
}
}
Now that we've moved our code to the handler we can update the existing code like so:
val helloHandler = HelloHandler()
client.on("messageCreate") { message ->
helloHandler.handleMessage(message)
}
Now we can call this code from a test. Let's make a quick test for testing a successful response:
class HelloGreetingTest {
@Test
fun testHelloWorldSuccess() {
val message = Message()
val helloHandler = HelloHandler()
helloHandler.handleMessage(message)
// assertTrue{ /* TODO: Assert the message was sent */ }
}
}
Hopefully this seems straight forward, though you may notice that we aren't asserting anything at the moment. This is because we don't have a way to track that a message has been sent, and we don't really want that in our HelloHandler
. For now it's commented out but we will update this in a later section.
Custom Messages
Another thing you may notice is there's no way to set the content of the Message
, this is because the Message
class is defined externally, which means that the bot is expecting the definition from the external Js.
Even if we didn't want a custom message, you cannot use external classes in testing, because there are variables that are expected but undefined
So how do we get around this?
Interfaces
The easiest way I've found to test external classes is to create a common interface. Then we can have two classes implementing this interface: the external class we already have, and a mocking class. Let's create an interface for Message
:
external interface MessageInterface {
val author: UserInterface
val channel: ChannelInterface
val content: String
}
Here we are defining the values we want to test, and we are setting it as external to match the existing Message
class.
Note: You don't need to define values you don't need, or else your interface would be massive for no good reason.
Now we'll have:
external class Message : MessageInterface {
override val author: User
override val channel: TextChannel
override val content: String
}
data class MockMessage(
override val author: MockUser,
override val channel: MockChannel,
override val content: String
) : MessageInterface
I've gone ahead and created a MockUser
and a MockChannel
, as we want to check the username for a bot name and check if the message was sent. These were created the same as the MockMessage
, by creating an interface and implementing it.
Note that all other
Mock
classes should be referencing the Interfaces you've created, not the external classes. Don't forget to update theHelloHandler
to use the interface.
So now we can test a custom message, great! How do we make sure our bot sent a greeting as a response?
Checking Responses
In order to test a response to an incoming message, we can create a MockChannel
to track when messages are sent. We don't want to actually send a message, just keep track of its status. I've created a MockChannel
that has a simple Boolean
that tells whether a message was sent.
class MockChannel : ChannelInterface {
var sentMessage: Boolean = false
private set
override fun send(content: Any): Promise<MessageInterface> {
print("Sent Message!")
sentMessage = true
return Promise { _, _ -> }
}
}
So now we can assert that the sentMessage
variable is true or false, depending on our test. You can go a step further and keep a reference to the information for other tests, but for simplicity we'll stick to this. Now we can finally go back to the test function and update it with our new code.
Final Test
@Test
fun testHelloWorldSuccess() {
val user = MockUser("Kevin")
val channel = MockChannel()
val message = MockMessage(user, channel, "Hello")
val helloHandler = HelloHandler()
helloHandler.handleMessage(message)
assertTrue{ message.channel.sentMessage }
}
Here we have a working test. We create the mock information, create the Handler, then handle the message. Then we assert that the bot sent a message back! Now let's run the test, by either clicking the arrow next to the test and selecting run
, or calling ./gradlew test
from the command line.
It failed!
Why did it fail? Let's check the HelloHandler
:
if(message.content == "hello")
message.channel.send("...")
It looks like we didn't take capitalization into account. Lets quickly change the check to message.equals("hello", ignoreCase = true)
, and run again.
It passed!
Excellent, our bot is working as we'd expect. We can go ahead and add more tests to make sure that other messages don't trigger a response, and catch other edge cases. Maybe we want to include exclamation points, or other languages, or we want to ignore hellos from users named Kevin. The possibilities are endless.
Conclusion
Congrats, You've tested your Discord Bot! You now have the basics to test all different messages and how your bot responds to them.
Your bot should now be running, stable, and ready to join some channels. This is the last post of the series, at this point you should have everything you need to know to be able to run a simple Discord Bot in KotlinJs. There are many other references out there to expand your bot from here using KMP libraries. Hopefully this series has been helpful and given you an introduction into kotlinJs.
Thanks for reading! Let me know in the comments if you have questions. Also, you can reach out to me at @kevinschildhorn on Twitter.
Posted on July 6, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.