ようへいの日々精進XP

よかろうもん

直近で Python の unittest で試行錯誤していて得られた知見の幾つか (3)

環境

以下のような環境でやってます.

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G19009

$ python --version
Python 3.6.4

前回の記事の続きです.

inokara.hateblo.jp

inokara.hateblo.jp

知見 (5) 例外発生をテストする

関数

以下のような関数を作成しました. 'hello' という引数が渡されることを想定していますが, 引数が無かったり, 'hello' 以外が引数として渡された際に例外が発生することを想定しています.

>>> def helloworld(args=None):
...     if args is None:
...         raise Exception('Noooooooooooooo Argumentttttttt.')
...     elif 'hello' not in args:
...         raise Exception('Invaliddddddddd Argumentttttttt.')
...     else:
...         return args
...
>>> helloworld()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in helloworld
Exception: Noooooooooooooo Argumentttttttt.
>>> helloworld('foo')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in helloworld
Exception: Invaliddddddddd Argumentttttttt.
>>> helloworld('hello')
hello

テスト

例外の発生をテストする場合には, assertRaises というアサーションを利用します. assertRaises は引数に例外クラス (SomeException) を指定します.

# 書き方 1
self.assertRaises(SomeException, do_something):

# 書き方 2
with self.assertRaises(SomeException):
    do_something()

以下のように assertRaises を利用したテストを書きました.

import unittest
import chiken

class HelloWorldTest(unittest.TestCase):
    def test_helloworld(self):
        self.assertEqual(chiken.helloworld('hello'), 'hello')

    def test_helloworld_no_argument_exception(self):
        self.assertRaises(Exception, chiken.helloworld, None)

    def test_helloworld_no_argument_exception_message(self):
        with self.assertRaises(Exception) as ex:
            chiken.helloworld(None)
        ex_message = ex.exception.args[0]
        self.assertEqual(ex_message, 'Noooooooooooooo Argumentttttttt.')

    def test_helloworld_invalid_argument_exception(self):
        self.assertRaises(Exception, chiken.helloworld, 'foo')

    def test_helloworld_invalid_argument_exception_message(self):
        with self.assertRaises(Exception) as ex:
            chiken.helloworld('foo')
        ex_message = ex.exception.args[0]
        self.assertEqual(ex_message, 'Invaliddddddddd Argumentttttttt.')

例外メッセージをテストしたい場合には, コンテキストマネージャを利用します.

...
    def test_helloworld_invalid_argument_exception_message(self):
        with self.assertRaises(Exception) as ex:
            chiken.helloworld('foo')
        ex_message = ex.exception.args[0]
        self.assertEqual(ex_message, 'Invaliddddddddd Argumentttttttt.')

コンテキストマネージャを利用した場合, 引数 exception で指定されたオブジェクトを格納する為, このオブジェクトを利用して例外発生時の詳細を確認することが出来ます. 上記の例では, 例外メッセージをオブジェクトから取得しています.

テストを実行する.

$ python -m unittest tests.test_chiken -v
test_helloworld (tests.test_chiken.HelloWorldTest) ... ok
test_helloworld_invalid_argument_exception (tests.test_chiken.HelloWorldTest) ... ok
test_helloworld_invalid_argument_exception_message (tests.test_chiken.HelloWorldTest) ... ok
test_helloworld_no_argument_exception (tests.test_chiken.HelloWorldTest) ... ok
test_helloworld_no_argument_exception_message (tests.test_chiken.HelloWorldTest) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

LGTM.

知見 (6) ログ出力をテストする

関数

先述の例外発生で利用した関数に logging モジュールを追加して, ログを出力するようにしました.

import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def helloworld2(args=None):
    if args is None:
        logger.error('Noooooooooooooo Argumentttttttt.')
    elif 'hello' not in args:
        logger.error('Invaliddddddddd Argumentttttttt.')
    else:
        print(args)

この関数を呼び出してみます.

>>> import logging
>>>
>>> logger = logging.getLogger()
>>> logger.setLevel(logging.INFO)
>>>
>>> def helloworld2(args=None):
...     if args is None:
...         logger.error('Noooooooooooooo Argumentttttttt.')
...     elif 'hello' not in args:
...         logger.error('Invaliddddddddd Argumentttttttt.')
...     else:
...         print(args)
...
>>> helloworld2()
Noooooooooooooo Argumentttttttt.
>>> helloworld2('foo')
Invaliddddddddd Argumentttttttt.
>>> helloworld2('hello world')
hello world

ということで, 意図したログが出力されることを確認したいと思います.

テスト

testfixtures

ログ出力を確認する為には, testfixtures モジュールの LogCapture クラスを利用します.

pypi.org

LogCapture クラスを利用することで, ログの出力を簡単にテストすることが出来るとのことです.

簡単に動作確認.

>>> import logging
>>> from testfixtures import LogCapture
>>> with LogCapture() as log:
...     logger = logging.getLogger()
...     logger.info('a message')
...
>>> log.check(('root', 'INFO', 'a message'))
>>>
>>> print(log)
root INFO
  a message
>>> log.check(('root', 'INFO', 'message'))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/path/to/.pyenv/versions/chiken.py/lib/python3.6/site-packages/testfixtures/logcapture.py", line 180, in check
    recursive=self.recursive_check
  File "/path/to/.pyenv/versions/chiken.py/lib/python3.6/site-packages/testfixtures/comparison.py", line 563, in compare
    raise AssertionError(message)
AssertionError: sequence not as expected:

same:
()

expected:
(('root', 'INFO', 'message'),)

actual:
(('root', 'INFO', 'a message'),)

テストコード

以下のようなテストを書きました.

class HelloWorldTest2(unittest.TestCase):
    def test_helloworld_no_argument_error_log(self):
        with LogCapture(level=logging.INFO) as log:
            chiken.helloworld2(None)
            log.check(('root', 'ERROR', 'Noooooooooooooo Argumentttttttt.'))

    def test_helloworld_invalid_error_log(self):
        with LogCapture(level=logging.INFO) as log:
            chiken.helloworld2('foo')
            log.check(('root', 'ERROR', 'Invaliddddddddd Argumentttttttt.'))

テストを実行してみます.

$ python -m unittest tests.test_chiken.HelloWorldTest2 -v
test_helloworld_invalid_error_log (tests.test_chiken.HelloWorldTest2) ... ok
test_helloworld_no_argument_error_log (tests.test_chiken.HelloWorldTest2) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

LGTM.

以上

知見でした.