Help us understand the problem. What is going on with this article?

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

More than 5 years have passed since last update.

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした