Tuesday, June 5, 2012

Bite-sized Twisted: Testing

In this post, we'll explore a few of the neat testing features Twisted has to offer. And we'll starting testing the Bomberman clone. I think we'll name the program boom.

Write tests the same way

You can write tests using unittest from the Python standard library. Read more about that here.

Let's make some bomb tests. I'm going to lay out the directory structure like this:

boom/
boom/__init__.py
boom/test
boom/test/__init__.py
boom/test/test_game.py

We'll make a really simple test for creating a Bomb:

from unittest import TestCase

from boom.game import Bomb

class BombTest(TestCase):

def test_init(self):
bomb = Bomb(3, 4)
self.assertEqual(bomb.fuse, 3)
self.assertEqual(bomb.size, 4)
boom/test/test_game.py

Trial

Twisted comes with an executable for running tests named trial. You can tell it to run tests found in files:

trial boom/test/test_game.py

Or tests found in modules/packages.

trial boom.test.test_game

Running either of the above will result in something like this:

boom
test
test_game ... [ERROR]

===============================================================================
[ERROR]
Traceback (most recent call last):
File "/usr/local/lib/python2.7/dist-packages/twisted/trial/runner.py", line 677, in loadByNames
things.append(self.findByName(name))
File "/usr/local/lib/python2.7/dist-packages/twisted/trial/runner.py", line 487, in findByName
return reflect.namedAny(name)
File "/usr/local/lib/python2.7/dist-packages/twisted/python/reflect.py", line 464, in namedAny
topLevelPackage = _importAndCheckStack(trialname)
File "/home/matt/iffycan.git/tx-bite-3-testing/boom/test/test_game.py", line 3, in <module>
from boom.game import Bomb
exceptions.ImportError: No module named game

boom.test.test_game
-------------------------------------------------------------------------------
Ran 1 tests in 0.071s

FAILED (errors=1)

Let's fix the test. This post isn't about testing as much as the tools Twisted provides, so I'm going to skip the iterations I went through to arrive at this file:

class Bomb:

def __init__(self, fuse, size):
self.fuse = fuse
self.size = size
boom/game.py

And now the tests pass. Yay!

boom.test.test_game
BombTest
test_init ... [OK]

-------------------------------------------------------------------------------
Ran 1 tests in 0.056s

PASSED (successes=1)
trial boom.test.test_game

Trial finds tests

As our program grows, it will be tedious to test if we have to type out the filename or module name for each of our test files. trial alleviates this by searching for things that look like tests. Generally, files with the word "test" in them are considered tests. We can run all of the boom tests just by specifying the root package (e.g trial boom):

boom.test.test_game
BombTest
test_init ... [OK]

-------------------------------------------------------------------------------
Ran 1 tests in 0.058s

PASSED (successes=1)
trial boom

Temporary files are easy

Twisted includes a subclass of unittest.TestCase which provides some very nice features. For instance, making temporary files and directories is easy using self.mktemp().

from twisted.trial.unittest import TestCase

class MkTempTest(TestCase):

def test_writeable(self):
filename = self.mktemp()
open(filename, 'w').write('foo')
test_mktemp.py

You can skip irrelevant tests

Sometimes you only want to run certain tests under certain conditions. Perhaps your code has optional features, or perhaps some tests only apply to certain operating systems. Twisted provides ways of skipping tests.

Skip entire TestCases by setting a skip attribute:

from twisted.trial.unittest import TestCase

class SkipTest1(TestCase):

skip = "Don't run these tests. They don't do anything"

def test_nothing(self):
pass
test_skip1.py
test_skip1
SkipTest1
test_nothing ... [SKIPPED]

===============================================================================
[SKIPPED]
Don't run these tests. They don't do anything

test_skip1.SkipTest1.test_nothing
-------------------------------------------------------------------------------
Ran 1 tests in 0.002s

PASSED (skips=1)
trial test_skip1.py

Skip individual tests by raising twisted.trial.unittest.SkipTest:

from twisted.trial.unittest import TestCase, SkipTest

class SkipTest2(TestCase):

def test_nothing(self):
raise SkipTest("Waste of time")
test_skip2.py
test_skip2
SkipTest2
test_nothing ... [SKIPPED]

===============================================================================
[SKIPPED]
Waste of time

test_skip2.SkipTest2.test_nothing
-------------------------------------------------------------------------------
Ran 1 tests in 0.028s

PASSED (skips=1)
trial test_skip2.py

Test runs are logged and saved

When you run trial, it creates or recreates a special directory named _trial_temp. Inside, you'll find test.log (a log of the test run). And you can inspect any files created with self.mktemp().

For instance, here's the test.log from running trial test_mktemp.py:

2012-06-02 17:20:27-0600 [-] Log opened.
2012-06-02 17:20:27-0600 [-] --> test_mktemp.MkTempTest.test_writeable <--
2012-06-02 17:20:27-0600 [-] using set_wakeup_fd

And there's a temporary file with foo in it, created by the test:

$ cat _trial_temp/test_mktemp/MkTempTest/test_writeable/d31a0y/temp 
foo

On this run, the file happened to end up in a directory named d31a0y/. This will vary from run to run and won't be the same when you run it.

Keep reading

I've highlighted just a few of my favorite trial features. All of the features described here do not involve the reactor and are a perfectly useable for testing synchronous code. A future post will discuss asynchronous testing features provided by Twisted. Read more about trial here.

I've created a GitHub repo for this Bomberman clone. The files used in this post are in the tx-testing-1 branch.

No comments:

Post a Comment