pythonでテストケース用のロガーを自作する

pythonでテストケース用のロガーを自作したので公開します

テスト関数にデコレータを付けるだけで,成功/失敗を記録するロガーを作りました.最小実装なので,好みに合わせて拡張できます.

ロガー本体のコードは末尾に掲載します.先に使い方から説明します.

使い方(その1)

ある関数 f に対して,テスト関数 test_simple を用意し,その引数と成功・失敗をログに出します.

# 自作ロガーをimport
from testcase import testcase

# テスト対象の関数
def f(x,y):
    return x+y

# テストケース."@testcase()”がデコレータ
@testcase()
def test_simple(x, y,  expected):
   return f(x, y) == expected

test_simple(1,2, 3) # f(1, 2)==3なので,これは成功
test_simple(2,4, 6) # f(2,4)==6なので,これも成功
test_simple(2,4, 4) #  これは失敗する

実行すると以下の出力が得られます

  OK test_simple(x=1, y=2, expected=3)  [0.00 msec]
  OK test_simple(x=2, y=4, expected=6)  [0.00 msec]
* NG test_simple(x=2, y=4, expected=4)  [0.00 msec]

先頭が NGで始まるテストケースは失敗を意味しています
末尾の [0.00 msec]は実行時間を表します

使い方(その2)

テストには次の2種類があります.

  • expected_success:通ることを期待するテスト
  • expected_failure:通らないことを期待するテスト(既知のバグなど)


上記の使い方(その1)は,expected_successのテストでした


expected_successとexpected_failureを使い分けるには次のようなコードを書きます

@testcase(expected_success=True)
def  test_expected_success(x, y):
      return f(x,y) == x+y

@testcase(expected_success=False)
def  test_expected_failure(x, y):
      return f(x,y) == x-y  


test_expected_success(1,2)
test_expected_success(3,4)
test_expected_success(0,0)

test_expected_failure(1,2)
test_expected_failure(3,4)
test_expected_failure(0,0)
  OK test_expected_success(x=1, y=2)  [0.00 msec]
  OK test_expected_success(x=3, y=4)  [0.00 msec]
  OK test_expected_success(x=0, y=0)  [0.00 msec]
  OK test_expected_failure(x=1, y=2)  [0.00 msec]
  OK test_expected_failure(x=3, y=4)  [0.00 msec]
* NG test_expected_failure(x=0, y=0)  [0.00 msec]


test_expected_failure(x, y) は式 x + y == x - y を評価し,Falseなら成功(OK),Trueなら失敗(NG) と判定します.
x + y == x - y は常にFalse……と見落としがちですが,x = y = 0 のときだけ True になるため,このケースは expected_failure で NG になります.

ポイント

このように,デコレータを付けるだけで,引数・実行結果・実行時間を一行で出力できます

また最小構成なので,出力先(標準出力/ファイル),色付け,集計(合計OK/NG,総時間)などが容易に拡張可能です.

ソースコード

import logging
import inspect
from functools import wraps
import time

logging.basicConfig(
    level=logging.INFO,
    format="%(message)s"
)
log = logging.getLogger("testcase")

def testcase(expected_success:bool = True):
    """
    """
    def decor(func):
        sig = inspect.signature(func)

        @wraps(func)
        def wrapper(*args, **kwargs):
            # 引数のログ
            bound = sig.bind_partial(*args, **kwargs)
            bound.apply_defaults()
            arg_str = ", ".join(f"{k}={repr(v)}" for k, v in bound.arguments.items())


            status = False
            t0 = time.perf_counter()
            try:
                result = bool(func(*args, **kwargs))
            except Exception as e:
                log.error(f"EXCEPTION in {func.__name__}: {e}")
            else:
                # 判定
                if expected_success:
                    status = (result is True)
                else:
                    status = (result is False)
            finally:
                delta_ms = (time.perf_counter() - t0) * 1000.0

            code = "  OK" if status else "* NG"
            log.info('%s %s(%s)  [%.2f msec]' % (code, func.__name__, arg_str, delta_ms))

            return status

        return wrapper
    return decor