Distest¶
Distest makes it easy to write application tests for discord bots.
Distest uses a secondary bot to send commands to your bot and ensure that it responds as expected.
See the interface reference for a list of assertions this library is capable of.
Note
Two quick note about recent changes:
- You NEED to enable the
members
intent on the tester bot. For more information, see Member Intent- If you’re using the
ext.commands.Bot
system, you will need to patch yourBot
to allow it to listen to other discord bots, as usually commands ignore other bots. This is really easy, we provide the patching function, just take a look at the patching documentation page.
Quickstart¶
Installation¶
- Install the library with pip:
$ pip install distest
Distest works by using a second bot (the ‘tester’) to assert that your bot (the ‘target’) reacts to events appropriately. This means you will need to create a second bot account through the Discord Developer’s Portal and obtain the authorization token. You also have to invite the tester to your discord guild. Additionally, be sure to enable the
SERVER MEMBERS INTENT
option, see Member Intent docs for more info.Refer to the Example Test Suite for the syntax/function calls necessary to build your suite.
Usage¶
The tests can be run in one of two modes: interactive and command-line. In interactive mode, the bot will wait for you to initiate tests manually. In command-line mode, the bot will join a designated channel, run all designated tests, and exit with a code of 0 if all tests were successful and any other number if the one or more tests failed. This allows for automating your test suite, allowing you to implement Continuous Integration on your Discord bot!
No matter how you run your tester, the file must contain:
- A call to
run_dtest_bot
, which will handle all command line arguments and run the tester in the correct mode - A
TestCollector
, which will let the bot find and run the you specify - One or more
Test
, which should be decorated with theTestCollector
, and are the actual tests that are run.
Note
The error codes will currently be 0 on success or 1 on failure, but we plan to implement meaningful error codes
Interactive Mode¶
- Run the bot by running your test suite module directly (called example_tester.py here):
$ python example_tester.py TARGET_ID TESTER_TOKEN
Go to the channel you want to run your tests in and call the bot using the
::run
command. You can either designate specific tests to run by name or use::run all
See also
::help
command for more commands/options.
Command-Line Mode¶
For command-line you have to designate the ID of the channel you want to run tests in (preceded by the -c
flag). You must also designate which
tests to run (with the -r
flag). Your command should look something like this:
$ python example_tester.py TARGET_ID TESTER_TOKEN -c CHANNEL_ID -r all
The program will print test names to the console as it runs them, and then exit.
See also
readme.md on GitHub, which contains a more in-depth look at the command properties
Example Test Suite¶
This is the example_tester.py
file found in the root directory. It contains tests for every assertion in Interface. This suite is also used to test our library, in conjunction with the example_target.py
.
The easiest way to get started is to adapt this suite of tests so it’s specific to your bot, then run this module with
$ python example_tester.py ${TARGET_ID} ${TESTER_TOKEN}
where TARGET_ID
is the Discord ID of your bot, and TESTER_TOKEN
is the auth token for your testing bot.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | """
A functional demo of all possible test cases. This is the format you will want to use with your testing bot.
Run with:
python example_tests.py TARGET_NAME TESTER_TOKEN
"""
import asyncio
import sys
from distest import TestCollector
from distest import run_dtest_bot
from discord import Embed, Member, Status
from distest import TestInterface
# The tests themselves
test_collector = TestCollector()
created_channel = None
@test_collector()
async def test_ping(interface):
await interface.assert_reply_contains("ping?", "pong!")
@test_collector()
async def test_delayed_reply(interface):
message = await interface.send_message(
"Say some stuff, but at 4 seconds, say 'yeet'"
)
await interface.get_delayed_reply(5, interface.assert_message_equals, "yeet")
@test_collector()
async def test_reaction(interface):
await interface.assert_reaction_equals("React with \u2714 please!", u"\u2714")
@test_collector()
async def test_reply_equals(interface):
await interface.assert_reply_equals("Please say 'epic!'", "epic!")
@test_collector()
async def test_channel_create(interface):
await interface.send_message("Create a tc called yeet")
created_channel = await interface.assert_guild_channel_created("yeet")
# @test_collector
# async def test_pin_in_channel(interface):
# await interface.send_message("Pin 'this is cool' in yeet")
# await interface.assert_guild_channel_pin_content_equals(created_channel )
@test_collector()
async def test_channel_delete(interface):
await interface.send_message("Delete that TC bro!")
await interface.assert_guild_channel_deleted("yeet")
@test_collector()
async def test_silence(interface):
await interface.send_message("Shhhhh...")
await interface.ensure_silence()
@test_collector()
async def test_reply_contains(interface):
await interface.assert_reply_contains(
"Say something containing 'gamer' please!", "gamer"
)
@test_collector()
async def test_reply_matches(interface):
await interface.assert_reply_matches(
"Say something matching the regex `[0-9]{1,3}`", r"[0-9]{1,3}"
)
@test_collector()
async def test_ask_human(interface):
await interface.ask_human("Click the Check!")
@test_collector()
async def test_embed_matches(interface):
embed = (
Embed(
title="This is a test!",
description="Descriptive",
url="http://www.example.com",
color=0x00FFCC,
)
.set_author(name="Author")
.set_thumbnail(
url="https://upload.wikimedia.org/wikipedia/commons/4/40/Test_Example_%28cropped%29.jpg"
)
.set_image(
url="https://upload.wikimedia.org/wikipedia/commons/4/40/Test_Example_%28cropped%29.jpg"
)
)
# This image is in WikiMedia Public Domain
await interface.assert_reply_embed_equals("Test the Embed!", embed)
@test_collector()
async def test_embed_part_matches(interface):
embed = Embed(title="Testing Title.", description="Wrong Description")
await interface.assert_reply_embed_equals(
"Test the Part Embed!", embed, attributes_to_check=["title"]
)
@test_collector()
async def test_reply_has_image(interface):
await interface.assert_reply_has_image("Post something with an image!")
@test_collector()
async def test_reply_on_edit(interface):
message = await interface.send_message("Say 'Yeah, that cool!'")
await asyncio.sleep(1)
await interface.edit_message(message, "Say 'Yeah, that is cool!'")
await interface.assert_message_contains(message, "Yeah, that is cool!")
@test_collector()
async def test_send_message_in_channel(interface):
message = await interface.send_message("Say stuff in another channel")
await interface.wait_for_message_in_channel("here is a message in another channel", 694397509958893640)
# Actually run the bot
if __name__ == "__main__":
run_dtest_bot(sys.argv, test_collector)
|
The Member Intent¶
Discord recently changed what you have to do for bots to be able to access server member informaiton, meaning that without changes, calling guild.members
will return an empty list, which is no good!!
To fix this, we need to do two things:
- Enable the Privileged Gateway Intent for Server Members.
- To do this. go to the Discord developer portal and select your tester bot
- Select the bot tab
- Enable the
SERVER MEMBERS INTENT
and thePRESENCE INTENT`
sliders
- Update distest! There are also changes that need to be made on our side. They have been made, but be sure you update to 0.4.9 or newer to get the changes!
Now, you should be good to go. Have fun testing!
Quick note - For some godforsaken reason, the on_member_update
event is just horribly slow and unreliable. I’m not really sure what to do about this, but be forewarned if you want to use it!
Main Functions¶
Interface¶
This is the most important class in the library for you, as it contains all the assertions and tools you need to interface with the library. Generally broken down into a few overall types:
- Message (i.e.
assert_message_contains
): Does not send it’s own message, so it require aMessage
to be passed in. - Reply (i.e.
assert_reply_contains
): Sends a message containing the text in contents and analyzes messages sent after that. - Use
get_delayed_reply
to wait an amount of time before checking for a reply
- Use
- Reply (i.e.
- Embed (i.e.
assert_embed_equals
): Sends a message then checks the embed of the response against a list of attributes - Other Tests (i.e.
ask_human
): Some tests do weird things and don’t have a clear category. - Interface Functions (i.e.
connect
,send_message
): Help other tests but also can be useful in making custom tests out of the other tests.
Enumerations¶
The following enumeration (subclass of enum.Enum
) is used to indicate the result of a run test.
Collector¶
Exceptions¶
Patches¶
Contains the code required to patch out the fact that Bot
class ignores messages
from other bots.
This should be used if you have a target bot that uses the ext.commands.Bot
system, as otherwise it’s commands will ignore messages from the tester bot.
Usage¶
Simply put the below code into your main bot and then when testing, the bot will no longer ignore other bots!
1 2 3 4 5 6 bot = commands.Bot(command_prefix='$') # Do anything you want for this if, be it env vars, command line args, or the likes. if sys.argv[2] == "TESTING": from distest.patches import patch_target bot = patch_target(bot)
Docs¶
Contributing¶
Hey! You’re here because you want to contribute to the bot, and that’s awesome! Here are some notes about contributions:
- Please open an issue for your contribution and tag it with contribution to discuss it. Because of my time constraints, I probably won’t have much time to implement new features myself, but if you make a feature and PR it in, I’ll be more than happy to spend a bit of time testing it out and add it to the project. The other thing is to make sure you check the github project to see if there is someone else already working on it who you can help.
- You may need to install the additional requirements from requirements-dev.txt. This is as simple as running pip install -r requirements-dev.txt. This larger list mostly includes things like black for formatting and sphinx for doc testing.
- If you are adding new test types, please make sure you test them well to make sure they work as intended, and please add a demo of them in use to the example_tests() for others to see. When you are done, please open a PR and I’ll add it in!
- I use Black for my code formatting, it would be appreciated if you could use it when contributing as well. I will remind you when you make a PR if you don’t, it is essential to make sure that diffs aren’t cluttered up with silly formatting changes. Additionally, CodeFactor should be tracking code quality and doing something with PRs. We will see soon exactly how that will work out.
- To build the docs for testing purposes, cd into the docs folder and run make testhtml. This will build to a set of HTML files you can view locally.
- Also, if you just want to propose an idea, create an issue and tag it with enhancement. The library is missing tons of features, so let me know what you want to see, and if I have time I’ll see about getting around to addign it. Thank you for your help!