Replies: 34 comments 17 replies
-
What exactly do you think Normally, this method sends an API request to Discord. How do you foresee that behaviour changing here, behind the scenes? |
Beta Was this translation helpful? Give feedback.
-
All REST requests (like |
Beta Was this translation helpful? Give feedback.
-
This is what I thought also - which means if you want to unit test a listener, you might as well just |
Beta Was this translation helpful? Give feedback.
-
@kyranet this is indeed the intended behaviour. |
Beta Was this translation helpful? Give feedback.
-
@monbrey that would work, however the code below throws an exception. import {Client, Guild, TextChannel, Message} from 'discord.js';
const client = new Client();
const guild = new Guild(client, {});
const channel = new TextChannel(guild, {});
const msg = new Message(channel, {}, client); Also its important to point out that the code I provided in the issue is simply an example, and I would expect the |
Beta Was this translation helpful? Give feedback.
-
What exception exactly? Something along the lines of required data missing from the Guild/Channel/Message you're trying to construct I assume, given that you're passing empty objects into real constructors. None of those classes have been mocked.
In theory sure, that still doesn't mean you can construct Guild/TextChannel/Message without providing any data. Mocked objects still require mock data. |
Beta Was this translation helpful? Give feedback.
-
you should mock the api, not the client. swapping out the logic of the client is likely to introduce bugs between the two. |
Beta Was this translation helpful? Give feedback.
-
Create a testing server (save the server id and channel id) and make a function that will run any command with the specified args passed. Run all variations of the command on the testing channel and assert that the last message sent was correct. Don't test more than one command at a time because sending too many messages is api abuse. But if you want a method of testing your discord bot, that is how I do it. |
Beta Was this translation helpful? Give feedback.
-
Well if testing was done by actually making a client login & performing the testing there, there can be API spam as pointed out by @Tenpi but also inconvenience. It would be slow & you may actually need to alter the source code to be able to test it which I don't think is a good thing. As to how the dummy data will be gained, new Discord.MockUser(userProperties: UserPropInterface = DefaultUserProp): MockUser
new Discord.MockChannel(channelProperties: ChannelPropInterface = DefaultChannelProp): MockChannel
new Discord.MockGuild(guildProperties: GuildPropInterface = DefaultGuildProp, users: MockUser[], channels: MockChannel[])
new Discord.MockClient(guilds: MockGuild[]): MockClient There will be no API calls done & all the return types of properties & methods would be the same... Everything would be synchronus (since there won't be any API calls) so it would be faster (but ofcourse promises returned to mock the API calls) |
Beta Was this translation helpful? Give feedback.
-
Lending my voice to say I would also be extremely interested in this. I've been bouncing some ideas around about this and creating some half-baked implementation ideas but haven't found a home run solution yet. Hitting a test server is great for end to end testing but it is far less useful for unit testing. |
Beta Was this translation helpful? Give feedback.
-
I'm still getting up to speed on the terminology / architecture of the library (so this might be way off base), but would it be feasible to add support for loading fixtures directly into the cache system? I'm wondering if it would be possible to essentially shim the state you want so all the methods don't hit the API to retrieve anything. |
Beta Was this translation helpful? Give feedback.
-
This is already possible. Collections (stable) and DataStores (master) extend Map.
|
Beta Was this translation helpful? Give feedback.
-
Maybe it's an error in my code but I haven't been able to get an example working - I tried to create two mock users, create a mock guild, create a mock text channel, and then assign the channel to the guild and assign the two users to the guild via GuildMember associations but was unsuccessful. I'm not entirely sure where the breakdown is happening but I think when you try to link together different structures it's still trying to hit an API somewhere. |
Beta Was this translation helpful? Give feedback.
-
It is actually possible to mock I needed to mock a
However there is only a small trick in order to properly resolve a guild from a message. Before instantiating a new Here is the full source-code of my mocking solution for my use-case, in TypeScript (click to expand).import Discord from 'discord.js';
export default class MockDiscord {
private client!: Discord.Client;
private guild!: Discord.Guild;
private channel!: Discord.Channel;
private guildChannel!: Discord.GuildChannel;
private textChannel!: Discord.TextChannel;
private user!: Discord.User;
private guildMember!: Discord.GuildMember;
private message!: Discord.Message;
constructor() {
this.mockClient();
this.mockGuild();
this.mockChannel();
this.mockGuildChannel();
this.mockTextChannel();
this.mockUser();
this.mockGuildMember();
this.guild.members.set(this.guildMember.id, this.guildMember);
this.mockMessage();
}
public getClient(): Discord.Client {
return this.client;
}
public getGuild(): Discord.Guild {
return this.guild;
}
public getChannel(): Discord.Channel {
return this.channel;
}
public getGuildChannel(): Discord.GuildChannel {
return this.guildChannel;
}
public getTextChannel(): Discord.TextChannel {
return this.textChannel;
}
public getUser(): Discord.User {
return this.user;
}
public getGuildMember(): Discord.GuildMember {
return this.guildMember;
}
public getMessage(): Discord.Message {
return this.message;
}
private mockClient(): void {
this.client = new Discord.Client();
}
private mockGuild(): void {
this.guild = new Discord.Guild(this.client, {
unavailable: false,
id: 'guild-id',
name: 'mocked discord.js guild',
icon: 'mocked guild icon url',
splash: 'mocked guild splash url',
region: 'eu-west',
member_count: 42,
large: false,
features: [],
application_id: 'application-id',
afkTimeout: 1000,
afk_channel_id: 'afk-channel-id',
system_channel_id: 'system-channel-id',
embed_enabled: true,
verification_level: 2,
explicit_content_filter: 3,
mfa_level: 8,
joined_at: new Date('2018-01-01').getTime(),
owner_id: 'owner-id',
channels: [],
roles: [],
presences: [],
voice_states: [],
emojis: [],
});
}
private mockChannel(): void {
this.channel = new Discord.Channel(this.client, {
id: 'channel-id',
});
}
private mockGuildChannel(): void {
this.guildChannel = new Discord.GuildChannel(this.guild, {
...this.channel,
name: 'guild-channel',
position: 1,
parent_id: '123456789',
permission_overwrites: [],
});
}
private mockTextChannel(): void {
this.textChannel = new Discord.TextChannel(this.guild, {
...this.guildChannel,
topic: 'topic',
nsfw: false,
last_message_id: '123456789',
lastPinTimestamp: new Date('2019-01-01').getTime(),
rate_limit_per_user: 0,
});
}
private mockUser(): void {
this.user = new Discord.User(this.client, {
id: 'user-id',
username: 'user username',
discriminator: 'user#0000',
avatar: 'user avatar url',
bot: false,
});
}
private mockGuildMember(): void {
this.guildMember = new Discord.GuildMember(this.guild, {
deaf: false,
mute: false,
self_mute: false,
self_deaf: false,
session_id: 'session-id',
channel_id: 'channel-id',
nick: 'nick',
joined_at: new Date('2020-01-01').getTime(),
user: this.user,
roles: [],
});
}
private mockMessage(): void {
this.message = new Discord.Message(
this.textChannel,
{
id: 'message-id',
type: 'DEFAULT',
content: 'this is the message content',
author: this.user,
webhook_id: null,
member: this.guildMember,
pinned: false,
tts: false,
nonce: 'nonce',
embeds: [],
attachments: [],
edited_timestamp: null,
reactions: [],
mentions: [],
mention_roles: [],
mention_everyone: [],
hit: false,
},
this.client,
);
}
} |
Beta Was this translation helpful? Give feedback.
-
Updated @TotomInc example for v12 Click to expand codeimport {
Client,
Guild,
Channel,
GuildChannel,
TextChannel,
User,
GuildMember,
Message,
} from "discord.js";
export default class MockDiscord {
private client!: Client;
private guild!: Guild;
private channel!: Channel;
private guildChannel!: GuildChannel;
private textChannel!: TextChannel;
private user!: User;
private guildMember!: GuildMember;
public message!: Message;
constructor() {
this.mockClient();
this.mockGuild();
this.mockChannel();
this.mockGuildChannel();
this.mockTextChannel();
this.mockUser();
this.mockGuildMember();
this.guild.addMember(this.user, { accessToken: "mockAccessToken" });
this.mockMessage();
}
public getClient(): Client {
return this.client;
}
public getGuild(): Guild {
return this.guild;
}
public getChannel(): Channel {
return this.channel;
}
public getGuildChannel(): GuildChannel {
return this.guildChannel;
}
public getTextChannel(): TextChannel {
return this.textChannel;
}
public getUser(): User {
return this.user;
}
public getGuildMember(): GuildMember {
return this.guildMember;
}
public getMessage(): Message {
return this.message;
}
private mockClient(): void {
this.client = new Client();
}
private mockGuild(): void {
this.guild = new Guild(this.client, {
unavailable: false,
id: "guild-id",
name: "mocked js guild",
icon: "mocked guild icon url",
splash: "mocked guild splash url",
region: "eu-west",
member_count: 42,
large: false,
features: [],
application_id: "application-id",
afkTimeout: 1000,
afk_channel_id: "afk-channel-id",
system_channel_id: "system-channel-id",
embed_enabled: true,
verification_level: 2,
explicit_content_filter: 3,
mfa_level: 8,
joined_at: new Date("2018-01-01").getTime(),
owner_id: "owner-id",
channels: [],
roles: [],
presences: [],
voice_states: [],
emojis: [],
});
}
private mockChannel(): void {
this.channel = new Channel(this.client, {
id: "channel-id",
});
}
private mockGuildChannel(): void {
this.guildChannel = new GuildChannel(this.guild, {
...this.channel,
name: "guild-channel",
position: 1,
parent_id: "123456789",
permission_overwrites: [],
});
}
private mockTextChannel(): void {
this.textChannel = new TextChannel(this.guild, {
...this.guildChannel,
topic: "topic",
nsfw: false,
last_message_id: "123456789",
lastPinTimestamp: new Date("2019-01-01").getTime(),
rate_limit_per_user: 0,
});
}
private mockUser(): void {
this.user = new User(this.client, {
id: "user-id",
username: "user username",
discriminator: "user#0000",
avatar: "user avatar url",
bot: false,
});
}
private mockGuildMember(): void {
this.guildMember = new GuildMember(
this.client,
{
deaf: false,
mute: false,
self_mute: false,
self_deaf: false,
session_id: "session-id",
channel_id: "channel-id",
nick: "nick",
joined_at: new Date("2020-01-01").getTime(),
user: this.user,
roles: [],
},
this.guild
);
}
private mockMessage(): void {
this.message = new Message(
this.client,
{
id: "message-id",
type: "DEFAULT",
content: "this is the message content",
author: this.user,
webhook_id: null,
member: this.guildMember,
pinned: false,
tts: false,
nonce: "nonce",
embeds: [],
attachments: [],
edited_timestamp: null,
reactions: [],
mentions: [],
mention_roles: [],
mention_everyone: [],
hit: false,
},
this.textChannel
);
}
} |
Beta Was this translation helpful? Give feedback.
-
Thanks, I understand now. Im kind of new to JS. Im trying to implement this mock in Jest and I have issues with calling |
Beta Was this translation helpful? Give feedback.
-
@leochen0000 Unfortunately, you would have to set the |
Beta Was this translation helpful? Give feedback.
-
I would try to set the https://discord.js.org/#/docs/main/master/class/GuildMember?scrollTo=permissions |
Beta Was this translation helpful? Give feedback.
-
A little bit off topic, but I came here wondering how to mock Discord bots with ts-jest. I used a mix of this excellent comment (#3576 (comment)) and this one (#3576 (comment)) to mock this working code out: describe("Bot", () => {
it("executes the help command", async () => {
let discordClient = new Discord.Client();
let guild = new Discord.Guild(discordClient, {
id: Discord.SnowflakeUtil.generate(),
});
let user = new Discord.User(discordClient, {
id: Discord.SnowflakeUtil.generate(),
});
let member = new Discord.GuildMember(
discordClient,
{ id: Discord.SnowflakeUtil.generate(), user: { id: user.id } },
guild
);
let role = new Discord.Role(
discordClient,
{ id: Discord.SnowflakeUtil.generate() },
guild
);
let message = new DiscordMessage(
discordClient,
{
content: "ab help",
author: { username: "BiggestBulb", discriminator: 1234 },
id: "test",
},
new Discord.TextChannel(new Guild(discordClient), {
client: discordClient,
guild: new Guild(discordClient),
id: "channel-id",
})
);
// Your testing code goes here, with your functions using the message passed in as if passed on-ready
const valid = Valid(message);
expect(valid).toBe(true);
});
}); Figured I would put it here for anyone in a similar situation as mine, works like a charm with Jest. |
Beta Was this translation helpful? Give feedback.
-
Does someone have a fix for this error without using
|
Beta Was this translation helpful? Give feedback.
-
@adrian-goe Set the const client = new Discord.Client({restSweepInterval: 0}) That makes sure Discord.js doesn't sent a timer (technically an interval) which would keep the Node.js event loop running. Side note: I'm working on a library that mocks Discord.js so it can be used for testing. |
Beta Was this translation helpful? Give feedback.
-
@cherryblossom000 thanks, that worked 😊 |
Beta Was this translation helpful? Give feedback.
-
So, should this issue be closed then? If I understand correctly, the requested feature will be implemented in this side-library, so this issue doesn't need to be open anymore. |
Beta Was this translation helpful? Give feedback.
-
@Poly-Pixel With the current codebase, yes this can be done using an external library. The way I'm approaching it is I'm overriding the // Something like this
import {Client} from 'discord.js'
import type {Snowflake} from 'discord.js'
const api = {
guilds: (id: Snowflake) => ({
// fetch guild
get: async () => ({
id,
name: 'blah',
// ...
})
}
class MockedClient extends Client {
constructor(options) {
super({...options, restSweepInterval: 0})
Object.defineProperty(this, 'api', {
value: api
})
}
} I think this issue should be moved to a discussion as to me this is more of a discussion on how to unit test Discord.js bots. However, it would be nice to have a less hacky way of doing this instead of relying on internal things like import type {
RESTGetAPIGuildResult,
RESTPostAPIChannelMessageFormDataBody,
RESTPostAPIChannelMessageJSONBody,
RESTPostAPIChannelMessageResult,
Snowflake
} from 'discord-api-types/v9'
interface DiscordService {
fetchGuild(id: Snowflake): Promise<RESTGetAPIGuildResult>
sendMessage(
channelId: Snowflake,
jsonBody: RESTPostAPIChannelMessageJSONBody,
formBody: RESTPostAPIChannelMessageFormDataBody
): Promise<RESTPostAPIChannelMessageResult>
// ...
}
class ActualDiscordService implements DiscordService {
constructor(client: Client) {
// init stuff like RequestManager
}
fetchGuild(id: Snowflake): Promise<RESTGetAPIGuildResult> {
// actually fetch guild
}
// ...
}
class Client {
constructor(options: ClientOptions, public service: DiscordService = new ActualDiscordService(this)) {
// ...
}
} |
Beta Was this translation helpful? Give feedback.
-
I agree, this should be moved to a discussion, but something like this could be helpful. |
Beta Was this translation helpful? Give feedback.
-
I believe for admins of the repo, there should be a "convert to discussion" option. |
Beta Was this translation helpful? Give feedback.
-
Hey @cherryblossom000, have you made any progress with the mocking library? I'll be more than happy to contribute. |
Beta Was this translation helpful? Give feedback.
-
#7693 might help with this |
Beta Was this translation helpful? Give feedback.
-
Hey, I was suggested to post something I've been working on to mock DiscordJS in this discussion It's not published yet but the source code is over at https://github.com/AnswerOverflow/AnswerOverflow/tree/main/packages/discordjs-mock The way it works is it uses reflect.construct to create the data you want to mock, and then you're able to pass in an override to that data I.e if you want to set a specific name. After it constructs them it populates the cache of the client. For things like .send() it just has the implement as a stub mock at the moment, but that can be expanded on to actually update the cache as well I'll probably turn it into its own MIT licensed package but for some example usage it looks like This example emits the guild member update event with a pending and non pending member let client: Client;
let members: GuildMemberVariants;
beforeEach(async () => {
client = await setupAnswerOverflowBot();
members = await createGuildMemberVariants(client);
});
it("should mark a pending user as consenting in a server with read the rules consent enabled", async () => {
// setup
await createServer({
...toAOServer(members.pendingGuildMemberDefault.guild),
flags: {
readTheRulesConsentEnabled: true,
},
});
// act
const fullMember = copyClass(members.pendingGuildMemberDefault, client);
fullMember.pending = false;
await emitEvent(
client,
Events.GuildMemberUpdate,
members.pendingGuildMemberDefault,
fullMember
);
await delay();
// assert
const updatedSettings = await findUserServerSettingsById({
userId: fullMember.id,
serverId: fullMember.guild.id,
});
expect(updatedSettings!.flags.canPubliclyDisplayMessages).toBe(true);
}); Full code of that is at https://github.com/AnswerOverflow/AnswerOverflow/blob/main/apps/discord-bot/src/listeners/events/read-the-rules-consent.test.ts If there's interest in this being it's own package please let me know along with any input on the structure of it |
Beta Was this translation helpful? Give feedback.
-
I see this discussion is still open 😅 Hi, @IanMitchell @Newbie012 @nopeless, I'm taking the helm of @cherryblossom000's mocking library and would love some help in getting it off the ground. If you're interested in contributing, this is the repo: gauntlet. I'm still getting it set up for Open Source development, but any help is welcome. |
Beta Was this translation helpful? Give feedback.
-
Seeing as the original issue (#727) was closed over a year ago (with no real conclusion to the issue), and unit testing is still a hassle to implement, I'm opening this issue.
Describe the ideal solution
The ideal solution would be for snippets like the one below to simply work:
Beta Was this translation helpful? Give feedback.
All reactions