Limnoria-doc/develop/advanced_plugin_testing.rst
2020-10-02 18:43:46 +02:00

331 lines
14 KiB
ReStructuredText

***********************
Advanced Plugin Testing
***********************
The complete guide to writing tests for your plugins.
Why Write Tests?
================
Why should I write tests for my plugin? Here's why.
For those of you asking "Why should I write tests for my plugin? I tried it
out, and it works!", read on. For those of you who already realize that
Testing is Good (TM), skip to the next section.
Here are a few quick reasons why to test your Supybot plugins.
* When/if we rewrite or change certain features in Supybot, tests make
sure your plugin will work with these changes. It's much easier to run
supybot-test MyPlugin after upgrading the code and before even reloading
the bot with the new code than it is to load the bot with new code and
then load the plugin only to realize certain things don't work. You may
even ultimately decide you want to stick with an older version for a while
as you patch your custom plugin. This way you don't have to rush a patch
while restless users complain since you're now using a newer version that
doesn't have the plugin they really like.
* Running the automated tests takes a few seconds, testing plugins in IRC
on a live bot generally takes quite a bit longer. We make it so that
writing tests generally doesn't take much time, so a small initial
investment adds up to lots of long-term gains.
* If you want your plugin to be included in any of our releases (the core
Supybot if you think it's worthy, or our supybot-plugins package), it has
to have tests. Period.
For a bigger list of why to write unit tests, check out this article:
http://www.onjava.com/pub/a/onjava/2003/04/02/javaxpckbk.html
and also check out what the Extreme Programming folks have to say about unit
tests:
http://www.extremeprogramming.org/rules/unittests.html
Plugin Tests
============
How to write tests for commands in your plugins.
Introduction
------------
This tutorial assumes you've read through the plugin author tutorial, and that
you used supybot-plugin-create to create your plugin (as everyone should). So,
you should already have all the necessary imports and all that boilerplate
stuff in test.py already, and you have already seen what a basic plugin test
looks like from the plugin author tutorial. Now we'll go into more depth about
what plugin tests are available to Supybot plugin authors.
Plugin Test Case Classes
------------------------
Supybot comes with two plugin test case classes, PluginTestCase and
ChannelPluginTestCase. The former is used when it doesn't matter whether or
not the commands are issued in a channel, and the latter is used for when it
does. For the most part their API is the same, so unless there's a distinction
between the two we'll treat them as one and the same when discussing their
functionality.
The Most Basic Plugin Test Case
-------------------------------
At the most basic level, a plugin test case requires three things:
* the class declaration (subclassing PluginTestCase or
ChannelPluginTestCase)
* a list of plugins that need to be loaded for these tests (does not
include Owner, Misc, or Config, those are always automatically loaded) -
often this is just the name of the plugin that you are writing tests for
* some test methods
Here's what the most basic plugin test case class looks like (for a plugin
named MyPlugin)::
class MyPluginTestCase(PluginTestCase):
plugins = ('MyPlugin',)
def testSomething(self):
# assertions and such go here
Your plugin test case should be named TestCase as you see above, though it
doesn't necessarily have to be named that way (supybot-plugin-create puts that
in place for you anyway). As you can see we elected to subclass PluginTestCase
because this hypothetical plugin apparently doesn't do anything
channel-specific.
As you probably noticed, the plugins attribute of the class is where the list
of necessary plugins goes, and in this case just contains the plugin that we
are testing. This will be the case for probably the majority of plugins. A lot
of the time test writers will use a bot function that performs some function
that they don't want to write code for and they will just use command nesting
to feed the bot what they need by using that plugin's functionality. If you
choose to do this, only do so with core bot plugins as this makes distribution
of your plugin simpler. After all, we want people to be able to run your
plugin tests without having to have all of your plugins!
One last thing to note before moving along is that each of the test methods
should describe what they are testing. If you want to test that your plugin
only responds to registered users, don't be afraid to name your test method
testOnlyRespondingToRegisteredUsers or testNotRespondingToUnregisteredUsers.
You may have noticed some rather long and seemingly unwieldy test method names
in our code, but that's okay because they help us know exactly what's failing
when we run our tests. With an ambiguously named test method we may have to
crack open test.py after running the tests just to see what it is that failed.
For this reason you should also test only one thing per test method. Don't
write a test method named testFoobarAndBaz. Just write two test methods,
testFoobar and testBaz. Also, it is important to note that test methods must
begin with test and that any method within the class that does begin with test
will be run as a test by the supybot-test program. If you want to write
utility functions in your test class that's fine, but don't name them
something that begins with test or they will be executed as tests.
Including Extra Setup
---------------------
Some tests you write may require a little bit of setup. For the most part it's
okay just to include that in the individual test method itself, but if you're
duplicating a lot of setup code across all or most of your test methods it's
best to use the setUp method to perform whatever needs to be done prior to
each test method.
The setUp method is inherited from the whichever plugin test case class you
chose for your tests, and you can add whatever functionality you want to it.
Note the important distinction, however: you should be adding to it and not
overriding it. Just define setUp in your own plugin test case class and it
will be run before all the test methods are invoked.
Let's do a quick example of one. Let's write a setUp method which registers a
test user for our test bot::
def setUp(self):
ChannelPluginTestCase.setUp(self) # important!!
# Create a valid user to use
self.prefix = 'foo!bar@baz'
self.feedMsg('register tester moo', to=self.nick, frm=self.prefix))
m = self.getMsg(' ') # Response to registration.
Now notice how the first line calls the parent class's setUp method first?
This must be done first. Otherwise several problems are likely to arise. For
one, you wouldn't have an irc object at self.irc that we use later on nor
would self.nick be set.
As for the rest of the method, you'll notice a few things that are available
to the plugin test author. self.prefix refers to the hostmask of the
hypothetical test user which will be "talking" to the bot, issuing commands.
We set it to some generically fake hostmask, and then we use feedMsg to send
a private message (using the bot's nick, accessible via self.nick) to the bot
registering the username "tester" with the password "moo". We have to do it
this way (rather than what you'll find out is the standard way of issuing
commands to the bot in test cases a little later) because registration must be
done in private. And lastly, since feedMsg doesn't dequeue any messages from
the bot after being fed a message, we perform a getMsg to get the response.
You're not expected to know all this yet, but do take note of it since using
these methods in test-writing is not uncommon. These utility methods as well as
all of the available assertions are covered in the next section.
So, now in any of the test methods we write, we'll be able to count on the
fact that there will be a registered user "tester" with a password of "moo",
and since we changed our prefix by altering self.prefix and registered after
doing so, we are now identified as this user for all messages we send unless
we specify that they are coming from some other prefix.
The Opposite of Setting-up: Tearing Down
----------------------------------------
If you did some things in your setUp that you want to clean up after, then
this code belongs in the tearDown method of your test case class. It's
essentially the same as setUp except that you probably want to wait to invoke
the parent class's tearDown until after you've done all of your tearing down.
But do note that you do still have to invoke the parent class's tearDown
method if you decide to add in your own tear-down stuff.
Setting Config Variables for Testing
------------------------------------
Before we delve into all of the fun assertions we can use in our test methods
it's worth noting that each plugin test case can set custom values for any
Supybot config variable they want rather easily. Much like how we can simply
list the plugins we want loaded for our tests in the plugins attribute of our
test case class, we can set config variables by creating a mapping of
variables to values with the config attribute.
So if, for example, we wanted to disable nested commands within our plugin
testing for some reason, we could just do this::
class MyPluginTestCase(PluginTestCase):
config = {'supybot.commands.nested': False}
def testThisThing(self):
# stuff
And now you can be assured that supybot.commands.nested is going to be off for
all of your test methods in this test case class.
Temporarily setting a configuration variable
--------------------------------------------
Sometimes we want to change a configuration variable only in a test (or in
a part of a test), and keep the original value for other tests. The
historical way to do it is::
import supybot.conf as conf
class MyPluginTestCase(PluginTestCase):
def testThisThing(self):
original_value = conf.supybot.commands.nested()
conf.supybot.commands.nested.setValue(False)
try:
# stuff
finally:
conf.supybot.commands.nested.setValue(original_value)
But there is a more compact syntax, using context managers::
import supybot.conf as conf
class MyPluginTestCase(PluginTestCase):
def testThisThing(self):
with conf.supybot.commands.nested.context(False):
# stuff
Plugin Test Methods
===================
The full list of test methods and how to use them.
Introduction
------------
You know how to make plugin test case classes and you know how to do just
about everything with them except to actually test stuff. Well, listed below
are all of the assertions used in tests. If you're unfamiliar with what an
assertion is in code testing, it is basically a requirement of something that
must be true in order for that test to pass. It's a necessary condition. If
any assertion within a test method fails the entire test method fails and it
goes on to the next one.
Assertions
----------
All of these are methods of the plugin test classes themselves and hence are
accessed by using self.assertWhatever in your test methods. These are sorted
in order of relative usefulness.
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.
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.
Other Tests
===========
If you had to write helper code for a plugin and want to test it, here's
how.
Previously we've only discussed how to test stuff in the plugin that is
intended for IRC. Well, we realize that some Supybot plugins will require
utility code that doesn't necessarily require all of the overhead of setting
up IRC stuff, and so we provide a more lightweight test case class,
SupyTestCase, which is a very very light wrapper around unittest.TestCase
(from the standard unittest module) that basically just provides a little
extra logging. This test case class is what you should use for writing those
test cases which test things that are independent of IRC.
For example, in the MoobotFactoids plugin there is a large chunk of utility
code dedicating to parsing out random choices within a factoid using a class
called OptionList. So, we wrote the OptionListTestCase as a SupyTestCase for
the MoobotFactoids plugin. The setup for test methods is basically the same as
before, only you don't have to define plugins since this is independent of
IRC.
You still have the choice of using setUp and tearDown if you wish, since those
are inherited from unittest.TestCase. But, the same rules about calling the
setUp or tearDown method from the parent class still apply.
With all this in hand, now you can write great tests for your Supybot plugins!