pythonのunittest用のmarkerライブラリを作りました

github.com

pythonのunittest用のmarkerライブラリを作りました

markerライブラリ?

特定のテストケースにマーカーを付けるためのライブラリです。 例えば、実行に時間がかかることが多いdbを利用したテストにマーカーをつけておいて、そのテストを避けてテストをするなどが挙げられます。

似たような機能としてpytestにはmarkerの機能が存在しています。

リンク先には、特定のテストにslowというマーカーをつけ、--runslowというオプションを付けなければslowのマーカーが付いたテストは実行しないという機能をどうやって実装するのかについての説明があります。

おおよそやりたいことはこれと同じようなことです。

testmarker?

pytestのmarkerの機能はただただマーカーをつけるだけの汎用的な機能なのですが。作ったライブラリのtestmarkerはもっと機能を絞っています。具体的には上の例であげたような実行される(skipされる)テストケースを指定することに用途を絞っています。

特徴をあげるとすると以下の様になります。

  • 標準ライブラリのみに依存
  • マーカーの利用はテストの実行/skipの指定に限定

install方法

installは通常通り以下です。

pip install testmarker

使いかた

使いかたを以下の2つの観点に分けて説明します。

  • テストの作成
  • テストの実行

テストの作成

テストの作成時には以下のように、testmarker.markを利用してテストケースやテストメソッドにマーカーを指定していきます。

test_it.py

import unittest
from testmarker import mark


@mark.a
class Test0(unittest.TestCase):
    def test_it(self):
        pass


class Test1(unittest.TestCase):
    @mark.a
    def test_it(self):
        pass

    @mark.b
    def test_it2(self):
        pass


class Test2(unittest.TestCase):
    def test_it(self):
        pass

上のコードでは以下2つのマーカーを指定しています。

  • a
  • b

テストの実行

テストの実行については説明するべきことが幾つかあります。マーカーの利用方法がいくつかあります。

  • 環境変数によるマーカーの指定
  • python -m testmarker discover での利用
  • python setup.py test からの利用

環境変数によるマーカーの指定

マーカーを指定するとそのマーカーの名前に対応した環境変数を通じてテストのskipを指定できます。例えば上の例ではaというマーカーを利用していたので以下の様に、NO_A=1という環境変数を指定して呼ぶことで、aのテストをスキップさせることができます(何がスキップされたかわかりやすいようにverboseオプションを付けています)。

$ NO_A=1 python -m unittest discover tests --verbose
test_it (test_it.Test0) ... skipped 'a'
test_it (test_it.Test1) ... skipped 'a'
test_it2 (test_it.Test1) ... ok
test_it (test_it.Test2) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK (skipped=2)

同様に、NO_B=1などとしてあげるとbのマーカーが指定されていたテストをスキップできます。

$ NO_A=1 NO_B=1 python -m unittest discover tests --verbose
test_it (test_it.Test0) ... skipped 'a'
test_it (test_it.Test1) ... skipped 'a'
test_it2 (test_it.Test1) ... skipped 'b'
test_it (test_it.Test2) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK (skipped=3)
デフォルトスキップのテストを有効にする

逆もまたできます。今までのマーカーはデフォルトの実行ではテスト対象に含まれていましたが、環境変数の指定によりスキップを行っていました。 逆に、デフォルトではスキップするテストケースを定義しておき、環境変数の指定により実行を許可するという形にもできます。

このときには、mark()時にskipオプションを付けてください

test_it2.py

import unittest
from testmarker import mark


@mark("x", skip=True)
class Tests(unittest.TestCase):
    def test_it(self):
        pass

デフォルトではスキップされます。

$ python -m unittest discover tests2 --verbose
test_it (test_it.Tests) ... skipped 'x'

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK (skipped=1)

xというマーカー名に対応したX=1というオプションを付けてあげるとスキップせずテストを実行してくれます。 (Xというのはマーカー名をstr.upper()した文字列です)

$ X=1 python -m unittest discover tests2 --verbose
test_it (test_it.Tests) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

まとめると以下の様になります

envname description
NO_<MARKER NAME> 対応するマーカーが指定されたテスト対象をスキップ
<MARKER NAME> 対応するマーカーが指定されたテストを実行対象に含める

python -m testmarker discover による実行

順番は前後してしまいますが。pythonの標準ライブラリのunittestを-m付きでコマンドラインから実行するとテストの実行を行うことができます。この機能と同様のインターフェイスpython -m testmarker discoverで実行できるようにしてみました。使えるオプションとして以下2つのオプションが増えます。

  • --ignore
  • --only
--ignore によるmarkerの指定

--ignore は実行から除外するmarkerの指定です。先ほどの環境変数を介した例でのNO_<MARKER NAME>と同様です。 例えば、a,bのマーカーの除外は以下の様にすることでも行えます。

$ python -m testmarker discover tests --verbose --ignore=a,b
test_it (test_it.Test0) ... skipped 'a'
test_it (test_it.Test1) ... skipped 'a'
test_it2 (test_it.Test1) ... skipped 'b'
test_it (test_it.Test2) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK (skipped=3)

## 以下の様にも書ける
$ python -m testmarker discover tests --verbose --ignore=a --ignore=b
--only によるmarkerの指定

--only は逆にこのオプションによって指定されたマーカーのみをテストの実行対象にするオプションです。--only--ignoreを同時に指定することはできません。

そして --onlyの実行で特殊なのはマーカーが設定されていないtest対象の扱いです。onlyというのはそれだけという意味なので、markerが指定されていなかったテストもまたスキップされます。

$ python -m testmarker discover tests --verbose --only=a
test_it (test_it.Test0) ... ok
test_it (test_it.Test1) ... ok
test_it2 (test_it.Test1) ... skipped 'b'
skipped 'Test2 is skipped by --only option'

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK (skipped=2)

元のコードでは、何もマーカーが指定されていなかったTest2が実行されていません。元のコードは以下の様なものです。

import unittest
from testmarker import mark


@mark.a
class Test0(unittest.TestCase):
    def test_it(self):
        pass


class Test1(unittest.TestCase):
    @mark.a
    def test_it(self):
        pass

    @mark.b
    def test_it2(self):
        pass


class Test2(unittest.TestCase):
    def test_it(self):
        pass

aでmarkされているTest0Test1.test_itだけが実行されています。

python setup.py test からの利用

これはおまけ的な機能でそれほど多くの人が使うとは思えませんが。pythonのsetup.py経由での実行でも先程の--only--ignoreが使えるようにできます。setup関数にcmdclassを渡してあげてください。

from setuptools import setup, find_packages
from testmarker.setupcmd import test

setup(
    name='foo',
    version='0.0',
    description='-',
    packages=find_packages(exclude=["foo.tests"]),
    test_suite="foo.tests",
    cmdclass={"test": test}
)

このようにすると。以下のような記述が可能になります。

$ python setup.py test --only=a

動作する実行例はexamplesにあります。