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.
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)
}
}
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)
...
})
}
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
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.