Skip to content

mavolin/dismock

Repository files navigation

dismock

GitHub Test Workflow Test Coverage Go Report Card PkgGoDev License


Dismock is a library that aims to make mocking Discord's API requests as easy as winking. No more huge integration tests that require a bot on some private server with little to no debug information.

Although, dismock uses arikawa as a foundation for its data types, it isn't limited to a specific discord library.

Getting Started

Basic Testing

You can create a mock by calling the method that corresponds to the API request you made in your code. Below is a simple example of a ping command, and it's unit test.

import (
    "github.com/diamondburned/arikawa/v3/bot"
    "github.com/diamondburned/arikawa/v3/gateway"
)

type Bot struct {
    Ctx *bot.Context
}

func (b *Bot) Ping(e *gateway.MessageCreateEvent) error {
    _, err := b.Ctx.SendText(e.ChannelID, "πŸ“")
    if err != nil {
        return err
    }
    _, err = b.Ctx.SendText(e.ChannelID, "Pong!")
    return err
}
import (
    "testing"

    "github.com/diamondburned/arikawa/v2/bot"
    "github.com/diamondburned/arikawa/v2/discord"
    "github.com/diamondburned/arikawa/v2/gateway"
    "github.com/mavolin/dismock/v2/pkg/dismock"
)

func TestBot_Ping(t *testing.T) {
    // you can also mock a Session by using dismock.NewSession(t), or dismock.New(t)
    // to only create a Mocker
    m, s := dismock.NewState(t)

    var channelID discord.ChannelID = 123

    m.SendText(discord.Message{
        // the doc of every mock specifies what fields are required, all other
        // fields not relevant to your test can be omitted
        ChannelID: channelID,
        Content:   "πŸ“",
    })

    // Mocks should be added in the same order their calls are made.
    // However, this order will only be enforced on calls to the same endpoint
    // using the same http method.
    m.SendText(discord.Message{
        ChannelID: channelID,
        Content:   "Pong!",
    })

    var b Bot
    _, _ = bot.New(s, &b)
    err := b.Ping(&gateway.MessageCreateEvent{
        Message: discord.Message{ChannelID: channelID},
    })
    if err != nil {
        t.Fatal(err)
    }
}

Advanced Testing

Now imagine a bit more complicated test, that has multiple sub-tests:

import (
    "github.com/diamondburned/arikawa/v2/bot"
    "github.com/diamondburned/arikawa/v2/gateway"
)

type Bot struct {
    Ctx *bot.Context
}

func (b *Bot) Ping(e *gateway.MessageCreateEvent) error {
    _, err := b.Ctx.SendText(e.ChannelID, "πŸ“")
    if err != nil {
        return err
    }

    _, err = b.Ctx.SendText(e.ChannelID, e.Author.Mention()+" Pong!")
    return err
}
func TestBot_Ping(t *testing.T) {
    m := dismock.New(t)

    var channelID discord.ChannelID = 123

    m.SendText(discord.Message{
        ChannelID: channelID,
        Content: "πŸ“",
    })

    t.Run("test1", func(t *testing.T) {
        // If you have multiple tests that make the same requests, you can
        // create a mocker, and add those API calls.
        // Afterwards, you can create a clone of the mocker in every sub-test 
        // you have.
        // Cloned mockers have a copy of their parent's request, but run their
        // own mock server and have a dedicated Session/State.
        m, s := m.CloneState(t)

        ...
    })

    t.Run("test2", func(t *testing.T) {
        m, s := m.CloneState(t)

        ...
    })
}

Using a Different Discord Library

Since mocking is done on a network level, you are free to chose whatever discord library you want. Simply use dismock.New when creating a mocker, replace the http.Client of your library of choice with mocker.Client, and disable the state.

Below is an example of using dismock with discordgo.

m := dismock.New(t)

s, _ := discordgo.New("Bot abc") // the token doesn't have to be valid
s.StateEnabled = false
s.Client = m.Client

Meta Requests

Besides regular calls to the API, you can also mock requests for metadata, i.e. images such as guild icons (Mocker.GuildIcon). In order for this to work you need to use the http.Client found in the Mocker struct, so that the mock server will be called instead of Discord.