noseで気軽にテストを書く(+geventの場合)

  • 86
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

pythonのテストフレームワークはいろいろありますが、個人的にはnoseというフレームワークがシンプルで好きですので、紹介したいと思います。

noseのインストール

$ pip install nose

環境に応じてsudoが必要かもしれないですね。

noseテストケースを書く

hogeというモジュールに以下のような関数sum, および関数is_evenがあると想定します。

hoge.py
def sum(a, b):
    return a + b

def is_even(n):
    return (n % 2 == 0)

unittest.TestCaseを継承した以下のようなファイルtest_hoge.pyを作成します。
ここでは値が期待する値と同じかどうかを調べるeq_と, 値が真であるかどうかを調べるok_nose.toolsからインポートして使っています。
テストフレームワークによくある仕組みとして、テストの前後に初期化/リソース解放などの処理を行うためのsetUp, tearDownが提供されていますので、試しにprint文を入れてみます。

test_hoge.py
from unittest import TestCase
from nose.tools import ok_, eq_
from hoge import sum, is_even

class HogeTestCase(TestCase):
    def setUp(self):
        print 'before test'
    def tearDown(self):
        print 'after test'

    def test_sum(self):
        eq_(sum(1, 2), 3)
        eq_(sum(5, 11), 16)
        eq_(sum(0, 0), 0)

    def test_is_even(self):
        ok_(is_even(2))
        ok_(not is_even(3))

テストケースを実行する

noseにはnosetestsというテストランナーコマンドがついてきます。(pip install noseするとインストールされます)
ここでnosetestsを実行すると以下のようになります。

$ nosetests
..
----------------------------------------------------------------------
Ran 2 tests in 0.003s

OK

nosetestsは実行されるとカレントディレクトリ以下からテストっぽい(testをファイル名に含む)ファイルを探し出し、unittest.TestCaseを継承するクラスをあつめて実行してくれます。
print文で出力したはずのメッセージが出ていませんが、これはデフォルトでnoseが標準出力への出力をキャプチャしてしまうからです。
-sオプションを使うと出力をそのまま見ることができます。

$ nosetests -s

実行してるテストケース名をドットだけでなく、メソッド名で表示させるときは-vオプションを付けます。

$ nosetests -v

あわせて実行してみると以下のようになりました。

$ nosetests -s -v
test_is_even (test_hoge.HogeTestCase) ... before test
after test
ok
test_sum (test_hoge.HogeTestCase) ... before test
after test
ok

----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK

nosetestsコマンドにはファイル名を指定して、特定のファイルのテストのみを実行させることもできます。
開発が進んできてテストが増えてくると、便利ですね。

$ nosetests ./test_hoge.py

その他のツール

ok_eq_の他のテストツールをいくつか紹介します。詳しくはnoseのTesting toolsのページを参照してください。

raises

raisesは例外が起きそうなテストに対してデコレータとして使います。
例外が起きそうな以下の関数divをテストします。

fuga.py
def div(n, m):
    return n / m

テストケースは以下。

fuga_hoge.py
from unittest import TestCase
from nose.tools import eq_, raises
from fuga import div
class FugaTestCase(TestCase):
    def test_div(self):
        eq_(div(10, 5), 2)
        eq_(div(10, 10), 1)
        eq_(div(9, 3), 3)

    @raises(ZeroDivisionError)
    def test_div_zerodiv(self):
        div(10, 0)


これでテストケースtest_div_zerodivがきちんと例外ZeroDivisionErrorを投げているかどうかを検査することができます。

timed

timedはデコレータで、テストケースが指定の時間内(秒で指定)に終わるかどうかを検査します。
以下のような、処理に0.1秒ほど時間のかかる関数do_somethingを、0.2秒以内で終わることを検査するテストを考えます。

moge.py
import time
def do_something():
    time.sleep(0.1)

テストケースは以下のようになります。

test_moge.py
from unittest import TestCase
from nose.tools import timed
from moge import do_something

class MogeTestCase(TestCase):
    @timed(0.2)
    def test_do_something(self):
        do_something()

geventを使ったプログラムのテストを書く

※geventなんらじゃらほい、という方はスルーでお願いします!

geventを使ったプログラムでは、以下のようなデコレータを使うとよいです。

gtestデコレータ
import gevent
def gtest(func):
    def _gtest(*args, **kwargs):
        gevent.spawn(func, *args, **kwargs).join()
    _gtest.__name__ = func.__name__
    return _gtest

以下のようなプログラムをテストしてみましょう。_tick_tacikが交互に動き、tick1, tack1, tick2, tack2というような番号つきの文字列を交互にキューに送信していき、最後にキューから中身を全部吸い出して配列にして返してくれます。

g.py
import gevent
import gevent.queue

def ticktack(n):
    q = gevent.queue.Queue()

    def _tick(_q, _n):
        for i in range(_n):
            _q.put('tick%d' % (i + 1))
            gevent.sleep()

    def _tack(_q, _n):
        for i in range(_n):
            _q.put('tack%d' % (i + 1))
            gevent.sleep()

    _tick_thread = gevent.spawn(_tick, q, n)
    _tack_thread = gevent.spawn(_tack, q, n)

    result = []
    while len(result) < n * 2:
        result.append(q.get())

    return result

gevent.sleep()が呼ばれたときにgeventが決定的なgreenlet切り替えを行うため、必ず先にspawnされたtick、あとからspawnされたtackが呼ばれます。

これを先のデコレータを使ってテストしてみましょう。

test_g.py
from unittest import TestCase
from nose.tools import eq_
import gevent

from g import ticktack

import gevent
def gtest(func):
    def _gtest(*args, **kwargs):
        gevent.spawn(func, *args, **kwargs).join()
    _gtest.__name__ = func.__name__
    return _gtest

class GTestCase(TestCase):
    @gtest
    def test_ticktack(self):
        r1 = ticktack(1)
        eq_(r1, ['tick1', 'tack1'])
        r2 = ticktack(2)
        eq_(r2, ['tick1', 'tack1', 'tick2', 'tack2'])

いい感じになりましたでしょうか?

まとめ

  • noseは気軽かつ簡単にテストケースを書ける
  • nosetestsはいろいろなオプションがあり実行も柔軟に行える
  • geventを使ったプログラムのテストケースも工夫すればok

お役に立てばうれしいです。