Adding tests to your Discord Bot - Discord Bot Series (Part 3)

kevinschildhorn

Kevin Schildhorn

Posted on July 6, 2022

Adding tests to your Discord Bot - Discord Bot Series (Part 3)

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"))
}
Enter fullscreen mode Exit fullscreen mode

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("...")
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
Enter fullscreen mode Exit fullscreen mode

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 */ }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 the HelloHandler 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 { _, _ -> }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 }
    }
Enter fullscreen mode Exit fullscreen mode

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("...")
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
kevinschildhorn
Kevin Schildhorn

Posted on July 6, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related