Unit tests for our IRC Bot application.
We’ll create a new file, test_quote_picker.py
to test our quote_picker.py
module within the tests
directory:
1 2 3 4 5 | (NetworkProj) $ mkdir tests
(NetworkProj) $ touch tests/__init__.py
(NetworkProj) $ touch tests/test_quote_picker.py
(NetworkProj) $ touch tests/test_quotes.txt
(NetworkProj) $ touch tests/test_talkbackbot.py
|
Open up tests/test_quote_picker.py
in your text editor.
A few import statements:
1 2 3 4 5 6 7 | import os
from twisted.trial import unittest
from talkback.quote_picker import QuotePicker
# <--snip-->
|
Let’s add a few test quotes to the test_quotes.txt
file. You can just copy & paste into that txt
file directly:
A fool without fear is sometimes wiser than an angel with fear. ~ Nancy Astor
You don't manage people, you manage things. You lead people. ~ Grace Hopper
Our TestQuotePicker class inherits Twisted’s unittest.TestCase
class, which is based off of Python’s unittest
library, but adds the ability to include Deferreds into our test suite (although deferreds are not needed when simply testing our QuotePicker
).
When we run our tests, TestCase
will run every function that starts with test
; in this case, it runs test_pick
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class TestQuotePicker(unittest.TestCase):
QUOTE1 = (
"A fool without fear is sometimes wiser than an angel with fear. "
"~ Nancy Astor"
)
QUOTE2 = (
"You don't manage people, you manage things. You lead people. "
"~ Grace Hopper"
)
def test_pick(self):
picker = QuotePicker(
os.path.join(os.path.dirname(__file__), "test_quotes.txt")
)
quote = picker.pick()
self.assertIn(quote, (self.QUOTE1, self.QUOTE2),
"Got unexpected quote: '%s'" % (quote))
|
We define a two constants, QUOTE1
and QUOTE2
. This is when we test our pick
function on our test_quotes.txt
file, we can be sure we actually pick a quote.
Only one function is defined in our quote_picker.py
(not including the init function), pick
; therefore, we only have one test case, test_pick
.
Within our test_pick
function, we instantiate QuotePicker
. Notice that we use the module os
to grab the test_quotes.txt
to pass into the QuotePicker
class. Rather than hard-coding the path to the quotes file, we take advantage of the os
standard module to:
os.path.dirname(__file__)
), andtest_quotes.txt
file by joining of the path to the current directory, and the file name itself.Next, we actually call our pick
method on the picker
object we instantiated.
Lastly, we need to make sure the pick
function returned what is expected. Grabbing a quote from the text_quotes.txt
file at (pseudo-)random should return one of the two quotes we defined earlier. We check this by using assertIn
function, where we make sure that the quote
we picked is one of the two quotes, QUOTE1
or QUOTE2
, and if not, to return the message: "Got unexpected quote: '%s'" % (quote))
.
Now open up test_talkbackbot.py
so we can write tests for our bot.py
module.
Let’s import some modules from Twisted for our testing, as well as the TalkBackBotFactory
that we want to test:
1 2 3 4 5 6 | from twisted.test import proto_helpers
from twisted.trial import unittest
from talkback.bot import TalkBackBotFactory
# <--snip-->
|
Remember from the intro that unit tests should be independent of other unit tests. Therefore, for the sake of our testing, we’ll define a contant, QUOTE
to “pick”, and create a dummy class for picking a quote, FakePicker
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # <--snip-->
QUOTE = "Nobody minds having what is too good for them. ~ Jane Austen"
class FakePicker(object):
"""
Always return the same quote.
"""
def __init__(self, quote):
self._quote = quote
def pick(self):
return self._quote
# <--snip-->
|
Let’s first start with the scaffolding for this unit test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # <--snip-->
class TestTalkBackBot(unittest.SynchronousTestCase):
_channel = "#testchannel"
_username = "tester"
_us = 'tbb'
def setUp(self):
def test_privmsgNoTrigger(self):
"""Shouldn't send a quote if message does not match trigger"""
def test_privmsgWithTrigger(self):
"""Should send a quote if message matches trigger"""
def test_privmsgAttribution(self):
"""If someone attributes the bot in public, they get a public response."""
def test_privmsgPrivateMessage(self):
"""For private messages, should send quote directly to user"""
|
Notice that we inherit from unittest.SynchronousTestCase
for our class. It simply extends unittest.TestCase
from Python’s standard library by adding some helpers, including logging, warning integration, monkey-patching (really!), and others.
Again, since unit tests need to be independent of each other, we will feed our TestTalkBackBot
some dummy private variables:
1 2 3 | _channel = "#testchannel"
_username = "tester"
_us = 'tbb'
|
Next, we create a function to actually setup the bot:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # <--snip-->
def setUp(self):
factory = TalkBackBotFactory(
self._channel,
self._us,
'Jane Doe',
FakePicker(QUOTE),
['twss'],
)
self.bot = factory.buildProtocol(('127.0.0.1', 0))
self.fake_transport = proto_helpers.StringTransport()
self.bot.makeConnection(self.fake_transport)
self.bot.signedOn()
self.bot.joined(self._channel)
self.fake_transport.clear()
# <--snip-->
|
The setUp
is from Python’s unittest
library that gets called to prepare our test fixture. Our setUp
function calls the TalkBackBotFactory
and initializes it with our dummy private variables we declared earlier.
We’re also building a fake protocol based off of localhost, 127.0.0.1
on port 0 in order to talk to our server. We create and connect to fake_transport
– fake_transport
emulates a network connection for us without actually connecting to a network.
In continuing our setUp
, we call signedOn
and joined
to connect to our fake IRC server, and clear any data received by the fake_transport
.
Onto our first test: test_privmsgNoTrigger
. We want to make sure our bot doesn’t respond with a quote if the message received does not match any listed trigger:
1 2 3 4 5 6 7 8 | # <--snip-->
def test_privmsgNoTrigger(self):
"""Shouldn't send a quote if message does not match trigger"""
self.bot.privmsg(self._username, self._channel, "hi")
self.assertEqual('', self.fake_transport.value())
# <--snip-->
|
Notice how the function starts with test_
; this is standard with Python unit tests. When we run our test suite, it will pick up on all functions that begin with test_
.
Now to test that our bot sends a quote if a message matches a trigger:
1 2 3 4 5 6 7 8 9 10 11 12 | # <--snip-->
def test_privmsgWithTrigger(self):
"""Should send a quote if message matches trigger"""
self.bot.privmsg(self._username, self._channel, "twss")
self.assertEqual(
'PRIVMSG {channel} :{username}: {quote}\r\n'.format(
channel=self._channel, username=self._username, quote=QUOTE
),
self.fake_transport.value())
# <--snip-->
|
The assertEqual
checks to see if the message, populated by channel
, username
, and quote
, is what is actually received by our fake transport.
Our next test will be testing when someone attributes the bot in the channel:
1 2 3 4 5 6 7 8 9 10 11 12 | # <--snip-->
def test_privmsgAttribution(self):
"""If someone attributes the bot in public, they get a public response."""
self.bot.privmsg(self._username, self._channel, self._us + ': foo')
self.assertEqual(
'PRIVMSG {channel} :{username}: {quote}\r\n'.format(
channel=self._channel, username=self._username, quote=QUOTE
),
self.fake_transport.value())
# <--snip-->
|
This just tests if a user pings our bot via the channel we’re in, and makes sure that the bot responds with a quote.
Our last test makes sure that we respond to a private message (via /msg
or /query
):
1 2 3 4 5 6 7 8 9 10 11 | # <--snip-->
def test_privmsgPrivateMessage(self):
"""For private messages, should send quote directly to user"""
self.bot.privmsg(self._username, self._us, "hi")
self.assertEqual(
'PRIVMSG {username} :{quote}\r\n'.format(
username=self._username, quote=QUOTE
),
self.fake_transport.value()
)
|
The complete test_talkbackbot.py
module:
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 | from twisted.test import proto_helpers
from twisted.trial import unittest
from talkback.bot import TalkBackBotFactory
QUOTE = "Nobody minds having what is too good for them. ~ Jane Austen"
class FakePicker(object):
"""Always return the same quote."""
def __init__(self, quote):
self._quote = quote
def pick(self):
return self._quote
class TestTalkBackBot(unittest.SynchronousTestCase):
_channel = "#testchannel"
_username = "tester"
_us = 'tbb'
def setUp(self):
factory = TalkBackBotFactory(
self._channel,
self._us,
'Jane Doe',
FakePicker(QUOTE),
['twss'],
)
self.bot = factory.buildProtocol(('127.0.0.1', 0))
self.fake_transport = proto_helpers.StringTransport()
self.bot.makeConnection(self.fake_transport)
self.bot.signedOn()
self.bot.joined(self._channel)
self.fake_transport.clear()
def test_privmsgNoTrigger(self):
"""Shouldn't send a quote if message does not match trigger"""
self.bot.privmsg(self._username, self._channel, "hi")
self.assertEqual('', self.fake_transport.value())
def test_privmsgWithTrigger(self):
"""Should send a quote if message matches trigger"""
self.bot.privmsg(self._username, self._channel, "twss")
self.assertEqual(
'PRIVMSG {channel} :{username}: {quote}\r\n'.format(
channel=self._channel, username=self._username, quote=QUOTE
),
self.fake_transport.value())
def test_privmsgAttribution(self):
"""If someone attributes the bot in public, they get a public response."""
self.bot.privmsg(self._username, self._channel, self._us + ': foo')
self.assertEqual(
'PRIVMSG {channel} :{username}: {quote}\r\n'.format(
channel=self._channel, username=self._username, quote=QUOTE
),
self.fake_transport.value())
def test_privmsgPrivateMessage(self):
"""For private messages, should send quote directly to user"""
self.bot.privmsg(self._username, self._us, "hi")
self.assertEqual(
'PRIVMSG {username} :{quote}\r\n'.format(
username=self._username, quote=QUOTE
),
self.fake_transport.value()
)
|