marshmallowで相互排他的なfieldを定義する方法
はじめに
こういうJSONを許したい。
{ "left": { "name": "foo", "value": 100, } }
あるいはこう。
{ "right": { "name": "foo", "value": 100.0, } }
left,rightというfieldの内どちらか1つだけ値が入るデータをvalidとしたい。例えば以下はダメ。
{}
これもだめ。
{ "left": { "name": "foo", "value": 100, }, "right": { "name": "foo", "value": 100.0, } }
2つのleft,rightというfieldの構造がテキトウ過ぎるけれど。2つ(Nつ)あるfieldの内1つだけに値が入るという状態にしたい。
方法
field自体にこのような機能をつけるのは無理で。validate_schemasというschemaレベルのvalidationを使う。
import marshmallow as ma class Item(ma.Schema): name = ma.fields.String(required=True) value = ma.fields.Integer(required=True) class Item2(ma.Schema): name = ma.fields.String(required=True) value = ma.fields.Number(required=True) class S(ma.Schema): left = ma.fields.Nested(Item) right = ma.fields.Nested(Item2) @ma.validates_schema def mutual(self, data): items = [item for item in [data.get("left"), data.get("right")] if item] if len(items) != 1: raise ma.ValidationError("items0 or items1") print(S().load({})) print(S().load({"left": {"name": "foo", "value": 10}})) print(S().load({"right": {"name": "foo", "value": 10}})) print(S().load({"left": {"name": "foo", "value": 10}, "right": {"name": "foo", "value": 10}})) # UnmarshalResult(data={}, errors={'_schema': ['items0 or items1']}) # UnmarshalResult(data={'left': {'value': 10, 'name': 'foo'}}, errors={}) # UnmarshalResult(data={'right': {'value': 10.0, 'name': 'foo'}}, errors={}) # UnmarshalResult(data={'right': {'value': 10.0, 'name': 'foo'}, 'left': {'value': 10, 'name': 'foo'}}, errors={'_schema': ['items0 or items1']})
pythonのunittest用のmarkerライブラリを作りました
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されているTest0
とTest1.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にあります。