Advanced Plugin Testing

Why Write Tests?

Automated testing is a hallmark of modern software development. Adding a test suite to your plugin makes it easier to update and redesign your code, update its dependencies, etc. without having to manually check that every feature still works.

Plugin Tests

First, we assume that you have already gone through the Plugin Development Tutorial and created the plugin boilerplate with supybot-plugin-create.

Plugin Test Case Classes

Limnoria comes with two plugin test case classes, supybot.test.PluginTestCase and supybot.test.ChannelPluginTestCase. Use the latter when the plugin’s commands need to be run in a channel, and the former otherwise.

Both of these classes inherit from Python’s built-in unittest.TestCase; as such, most of the documentation there applies to plugin tests too.

Note that instead of python -m unittest, Limnoria plugin tests are run using the limnoria-test command: e.g. limnoria-test /path/to/your/Plugin

Plugin Test Case Example

A basic plugin test case requires:

  • The class declaration (subclassing one of the TestCase classes above)

  • A list of plugins to be loaded for these tests. Usually this is just the name of the plugin you’re testing. Note that the Owner, Misc, and Config plugins are always automatically loaded

  • Some test methods

class MyPluginTestCase(PluginTestCase):
    # List of plugins to load
    plugins = ('MyPlugin',)

    def testEcho(self):
        # Replace the command and expected response with your own,
        # add other assertions, etc.
        self.assertResponse('echo Hello world', 'Hello world')

    def testSomethingElse(self):
        # Add another test case here

As with Python’s unittest module in general, test methods must begin with test. When adding helper methods in this class, they should start with something else.

When writing your actual test methods, we recommend keeping each one short and targeted towards a specific feature. This will allow you to easily find which checks failed just by looking at a failed test’s name, instead of having to sort through line numbers inside test.py. To keep test functions unambiguous, it is fine to give them longer, more specific names to compensate (e.g. testOnlyRespondToRegisteredUsers instead of testRegistered).

Including Extra Setup

Plugin tests may define extra setup commands by overriding setUp() from Python’s unittest module:

    def setUp(self):
        # Important! This sets up the bot's simulated IRC network for testing
        super().setUp()

        # Define the identity of the user who we send messages as
        self.prefix = 'foo!bar@baz'

        # Send a message to the simulated IRC network, in this case to register
        # an account with the bot. self.nick refers to the bot's nick.
        self.feedMsg('register tester moo', to=self.nick, frm=self.prefix)
        m = self.getMsg(' ')  # Get the response for the last command

If your setUp function does work that should be cleaned up after, add a tearDown method as well. As with setUp, you should also call the parent class’ tearDown method after running your own cleanup.

Setting Config Variables for Testing

Config variables can be set at the test case level. For example, to disable nested commands for this test, you can add a config dict:

class MyPluginTestCase(PluginTestCase):
    config = {'supybot.commands.nested': False}

    def testThisThing(self):
        # stuff

Temporarily setting a configuration variable

To temporarily set a config variable inside a test method, use the conf.supybot.<variable name>.context(<new value>) context manager:

import supybot.conf as conf

class MyPluginTestCase(PluginTestCase):
    def testThisThing(self):
        with conf.supybot.commands.nested.context(False):
            # stuff
        # when leaving the context manager, the config value is reverted to default

Plugin Test Methods

In addition to Python’s built-in assertions, here are all the test methods defined in Limnoria. These are instance methods, so they should be accessed as self.assertResponse(...), etc.

assertResponse(query, expectedResponse)

Feeds query to the bot as a message and checks to make sure the response is expectedResponse. The test fails if they do not match (note that prefixed nicks in the response do not need to be included in the expectedResponse).

assertError(query)

Feeds query to the bot and expects an error in return. Fails if the bot doesn’t return an error.

assertNotError(query)

The opposite of assertError. It doesn’t matter what the response to query is, as long as it isn’t an error. If it is not an error, this test passes, otherwise it fails.

assertRegexp(query, regexp, flags=re.I)

Feeds query to the bot and expects something matching the regexp (no m// required) in regexp with the supplied flags. Fails if the regexp does not match the bot’s response.

Note

This assertRegexp() function is not the same as assertRegex() from Python’s unittest library. assertRegex() compares a regexp against a bare string, while assertRegexp() compares it to the output of a bot command. (For historical reasons, we have this confusing name.)

assertNotRegexp(query, regexp, flags=re.I)

The opposite of assertRegexp. Fails if the bot’s output matches regexp with the supplied flags.

assertHelp(query)

Expects query to return the help for that command. Fails if the command help is not triggered.

assertAction(query, expectedResponse=None)

Feeds query to the bot and expects an action in response, specifically expectedResponse if it is supplied. Otherwise, the test passes for any action response.

assertActionRegexp(query, regexp, flags=re.I)

Basically like assertRegexp but carries the extra requirement that the response must be an action or the test will fail.

Utilities

feedMsg(query, to=None, frm=None)

Simply feeds query to whoever is specified in to or to the bot itself if no one is specified. Can also optionally specify the hostmask of the sender with the frm keyword. Does not actually perform any assertions.

getMsg(query)

Feeds query to the bot and gets the response.

Tests for Helper Code

If you want to test plugin helpers individually without running commands from your commands, you can add additional test classes inheriting from supybot.test.SupyTestCase. This is a light wrapper around unittest.TestCase that provides some additional logging.

The MoobotFactoids plugin has an example of this (OptionListTestCase).

The same rules for using setUp and tearDown apply: be sure to call the parent class implementations in your overridden functions.