#デバッグとユニットテスト
assert文
変数testが偽ならAssertionError例外を発生させる。
test = 0
data = "assertion error"
try:
assert test,data
except AssertionError:
print data
finally:
print "the end"
assert文の部分のみなら、次のように書く事で代用もできる。
if __debug__:
if not test
raise AssertionError, data
例外の分離処理や内容の表示
somedata = 1
# キャッチしたい例外をタプルでまとめておく。
fatal_exceptions = (KeyboardInterrupt, MemoryError)
try:
assert somedata
except fatal_exceptions, inst: # 引数instで例外の内容を受け取ることができる。
print type(inst) # 例外の型を表示
print inst # 例外の内容を表示
raise
# それ以外の例外をまとめてキャッチ
except Exception, inst:
print type(inst) # 例外の型を表示
print inst # 例外の内容を表示
finally:
print "the end"
UnitTest
公式のチュートリアルからの例を少し修正と追加。
import random
import unittest
class TestSequenceFunctions(unittest.TestCase):
# 毎回呼ばれる初期化処理
# この他にテスト実行後などに呼ばれるメソッドがある
def setUp(self):
self.seq = range(10)
# testで始まるメソッド名を記述する。
def test_shuffle(self):
random.shuffle(self.seq)
self.seq.sort()
# 二つの引数がイコールであることをチェックする。
# 等しくないことのチェックはassertNotEqual()で行える。
self.assertEqual(self.seq, range(10))
# 例外が発生することのチェックを行う。
# assertRaises(exception, callable, *args, **kwds)
# 第二引数の関数にargsとkwdsを渡して、第一引数で指定した例外が発生することをチェックする。
self.assertRaises(TypeError, random.shuffle, (1,2,3))
def test_choice(self):
element = random.choice(self.seq)
# 引数の値がTrueであることをチェックする。
# bool(element) is True と等価
self.assertTrue(element in self.seq)
def test_sample(self):
# exception 引数のみが渡された場合には、コンテキストマネージャが返される。
# インラインでテスト対象のコードを書くことができる。
with self.assertRaises(ValueError):
random.sample(self.seq, 20)
for element in random.sample(self.seq, 5):
self.assertTrue(element in self.seq)
if __name__ == '__main__':
# main()で実行できる。
unittest.main()
# 個別に実行するテストを取得する。
_test_choice = TestSequenceFunctions('test_choice')
_test_sample = TestSequenceFunctions('test_sample')
# テストスイートに登録してランナーでまとめて実行できる。
TestSuite = unittest.TestSuite()
TestSuite.addTest(_test_choice)
TestSuite.addTest(_test_sample)
runner = unittest.TextTestRunner()
runner.run(TestSuite)
# ローダーでまとめてテスト関数を取得できる。
suite = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions)
unittest.TextTestRunner(verbosity=2).run(suite)
traceback
スタックとは現在プログラムを実行しているメモリ領域である。
スタックトレースとは、そのメモリ領域のプログラムの実行状態(関数名、呼び元関数名、行、命令文、ファイル名)などの状態を記録したものであり、Pythonのようなスクリプト言語はこれがオブジェクトとして提供されている。
まずスタックトレースの情報はsys.exc_info() から取得することができる。
exc_type, exc_value, exc_traceback = sys.exc_info()
この三つの変数を引数に使用して、traceback.print_tb()やtraceback.print_exception()で情報を表示する。
しかし、下記のtraceback.print_exc()を使うと、sys.exc_info()からの変数取得が省略できて、しかもタイプや内容も表示できるので便利。基本的にはこれを使用すればいい。
import sys
import traceback
def somework():
try:
print a # 定義されていない変数を使用する
except Exception:
print "error"
traceback.print_exc(file=sys.stdout)
finally:
print "the end"
if __name__ == '__main__':
somework()
出力結果
モジュール名、行、関数名、原因の命令文、原因のエラーメッセージが出力される。
error
Traceback (most recent call last):
File "/Users/test.py", line 8, in somework
print a
NameError: global name 'a' is not defined
the end
こちらはtracebackを書かなかった場合の出力。
例外を通過するだけで何も出力されない。
error
the end
次に、スタックトレース情報をタプルを含んだリストで取得する方法を示す。
こちらはsys.exc_info()から変数を取得する例も載せておく。(それを引数に使っているから)
import sys
import traceback
def somework():
try:
print a # 定義されていない変数を使用する
except Exception:
print "error"
exc_type, exc_value, exc_traceback = sys.exc_info()
print traceback.extract_tb(exc_traceback) # 現在の情報だけを取得
print traceback.extract_stack() # 呼び出し元の関数情報を含むタプルを取得
# タプルではなく単にリストで受け取って表示しやすくしたい場合はこれを呼ぶ。
traceback.format_tb(exc_traceback)
traceback.format_stack()
print "the end"
if __name__ == '__main__':
somework()
#nosetest
『Pythonプロフェッショナルプログラミング』を参考にさせていただきました。
noseのインストール
pip install nose
テスト対象のクラス
このクラスは単純に銀行のアカウントに数字を足したり減らしたりするだけ。
# coding: UTF-8
class NotEnoughFundsException(Exception):
pass
class BankAccount(object):
def __init__(self):
self._balance = 0
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
self.balance -= amount
def get_balance(self):
return self._balance
def set_balance(self, value):
if value < 0:
raise NotEnoughFundsException
self._balance = value
# ゲッターとセッターを使えるようにラッパーを用意する。
# balanceに=や+でアクセスすることでこれらの関数が自動で呼ばれるようになる。
balance = property(get_balance, set_balance)
noseでテストするコードを書く。
ここで100や0などのテスト用の固定の値を準備することをデータフィクスチャという。
# coding: UTF-8
import unittest
class BankAccountTest(unittest.TestCase):
def _getTarget(self):
from bankaccount import BankAccount
return BankAccount
# テスト対象のインスタンスを作成する。
def _makeOne(self, *args, **kwargs):
return self._getTarget()(*args, **kwargs)
def test_construct(self):
target = self._makeOne()
self.assertEqual(target._balance, 0)
def test_deposit(self):
target = self._makeOne()
target.deposit(100)
self.assertEqual(target._balance, 100)
def test_withdraw(self):
target = self._makeOne()
target._balance = 100
target.withdraw(20)
self.assertEqual(target._balance, 80)
def test_get_blance(self):
target = self._makeOne()
target._balance = 500
self.assertEqual(target.get_balance(), 500)
def test_set_balance_not_enough_funds(self):
target = self._makeOne()
from bankaccount import NotEnoughFundsException
try:
target.set_balance(-1)
self.fail()
except NotEnoughFundsException:
pass
上記二つを同じディレクトリに配置し、そのディレクトリでnoseを実行する。
nosetest
.....
----------------------------------------------------------------------
Ran 5 tests in 0.004s
OK
カバレッジの取得
pip install coverage
# nosetests -v --with-coverage
test_construct (test_bankaccount.BankAccountTest) ... ok
test_deposit (test_bankaccount.BankAccountTest) ... ok
test_get_blance (test_bankaccount.BankAccountTest) ... ok
test_set_balance_not_enough_funds (test_bankaccount.BankAccountTest) ... ok
test_withdraw (test_bankaccount.BankAccountTest) ... ok
Name Stmts Miss Cover Missing
-------------------------------------------
bankaccount 16 0 100%
----------------------------------------------------------------------
Ran 5 tests in 0.006s
OK
見やすいカバレッジが出力されます。
「--with-xunit」オプションでXUNIT形式で出力する。
# nosetests -v -w . --with-coverage --with-xunit
test_construct (test_bankaccount.BankAccountTest) ... ok
test_deposit (test_bankaccount.BankAccountTest) ... ok
test_get_blance (test_bankaccount.BankAccountTest) ... ok
test_set_balance_not_enough_funds (test_bankaccount.BankAccountTest) ... ok
test_withdraw (test_bankaccount.BankAccountTest) ... ok
----------------------------------------------------------------------
XML: nosetests.xml
Name Stmts Miss Cover Missing
-------------------------------------------
bankaccount 16 0 100%
----------------------------------------------------------------------
Ran 5 tests in 0.006s
OK
jenkinsで読めるようにXMLで出力する。
coverage xml
#mock
普通の使い方
テスト対象のクラス
testモジュールのTest.py
# coding: UTF-8
class Widget(object):
def __init__(self):
self.value = 10
def Additional(self, add):
self.value += add
return self.value
テストコード
from mock import Mock
from test import Test
def mock_value(value):
return 20 + value
if __name__ == '__main__':
# 関数を置き換えて、100を固定で返すモックにする。
Test.Widget.Additional = Mock(return_value=100)
w = Test.Widget()
print w.Additional(1)
# 固定値ではなく、なんらかの計算をする場合はside_effectでダミー関数を渡す。
Test.Widget.Additional = Mock(side_effect=mock_value)
print w.Additional(1)
100
21
クラスをモックに置き換える
from mock import Mock
from test import Test
def mock_value(value):
return 20 + value
if __name__ == '__main__':
# クラス自体を置き換える。
Test.Widget = Mock()
# 関数の返す値を固定してしまう。
Test.Widget.return_value.Additional.return_value = 10
w = Test.Widget()
print w.Additional(1)
# 関数を入れ替える
Test.Widget.return_value.Additional = mock_value
print w.Additional(1)
patch
関数実行中のmockオブジェクトと本物のオブジェクトを差し替える。
# coding: UTF-8
from mock import patch
# この関数実行中だけ置き換えるメソッドと戻り値を指定
@patch("Test.Widget.Additional", return_value=10)
def test_func(m):
import Test
w = Test.Widget()
print w.Additional(1)
assert w.Additional(1) == 10
# ちなみに引数で受け取ったmはモックに置き換えられたAdditional関数
# だからこちらも結果は10になる。
# 今回はメソッドを置き換えたが、クラスの場合はクラスになる。
print m()
if __name__ == '__main__':
test_func()
withを使ったコンテキストマネージャ形式の場合
# coding: UTF-8
from mock import patch
def test_func():
# このwithスコープ実行中だけ置き換えるメソッドと戻り値を指定
with patch("Test.Widget.Additional", return_value=10) as m:
import Test
w = Test.Widget()
print w.Additional(1)
assert w.Additional(1) == 10
print m()
if __name__ == '__main__':
test_func()
クラス型指定
mock = Mock(spec=SomeClass)
isinstance(mock, SomeClass) #これが成功する
Mockに特定のアトリビュートを持たせる
# coding: UTF-8
from mock import Mock, patch
if __name__ == '__main__':
# methodという名前で3を返すメソッドと、
# otherという名前でKeyError例外を発生させるメソッド
attrs = {'method.return_value': 3, 'other.side_effect': KeyError}
# 宣言と同時にアトリビュート追加もできる(some_attribute)。
mock = Mock(some_attribute='eggs', **attrs)
print mock.some_attribute
print mock.method()
print mock.other()
その他
他に便利そうなのは、
@patch('sys.stdout', new_callable=StringIO)
で、生成時にStringIOのオブジェクトとして生成してくれる。
わざわざmockを使わなくてもテスト用の疑似クラスを書いて「Test.Widget = 」で代入してもいい。mockのバリデーション機能を使わないのなら、そちらの手段の方がはやいかもしれない。
#django-webtestモジュール
Djangoのテストに非常に便利です。
pip install webtest
pip install django-webtest
Djangoのアプリケーションは次のようになっている。
mysite
|-mysite
| |-__init__.py
| |-settings.py
| |-urls.py
| |-wsgi.py
|-test app
| |-__init__.py
| |-form.py
| |-modes.py
| |-views.py
| |-tests.py このファイルに今回のテストを書く|
|-templates
|-manage.py
tests.pyの内容を次のように書く
from django_webtest import WebTest
class TestIndex(WebTest):
def test_index(self):
res = self.app.get("/") #レスポンスを取得する
# ステータスコードやコンテンツ内容をチェックする。
assert res.status == '200 OK'
assert 'html' in res
manage.py runserverで開発サーバーを立ち上げておいてから、次のコマンドでテストを実行する。
# sudo python manage.py test testapp
出力結果
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.175s
OK
Destroying test database for alias 'default'...