https://us.pycon.org/2012/ に参加してきました。
そのときのイベントレポートを 「PyCon US 2012」 イベントレポート:レポート|gihyo.jp … 技術評論社 に書きました。face to face でコミュニケーションを取ることの重要さを実感しました。
tox と pytest で Python 2/3 両対応のテストを実行する
pytest *1 に関連して tox も一緒に覚えておくと良さそうです。
@t2y @methane 少し前からtox + py.test が鉄板な気がしますねえ
2012-02-07 20:50:23 via twicca to @t2y
tox については id:Ehren の入門記事が分かりやすいです。
複数の Python バージョン毎に virtualenv で仮想環境を作成して、そこに自分のパッケージと必要なライブラリ等をインストールして、それぞれのバージョン毎にテストをまとめて実行してくれます。例えば、Python 2/3 の両対応を考えたときに、自分で環境を切り分ける手間隙を軽減できて、かなり便利です。ここで言う Python 2/3 両対応は 2.6/2.7 と 3.x の対応を指します *2 。
環境
(test)$ pip freeze distribute==0.6.24 py==1.4.7 pytest==2.2.3 tox==1.3 virtualenv==1.7 wsgiref==0.1.2
bitbucket にこの記事で紹介するテストコードを置きました。興味のある方は試してみてください。
設定
wikipedia:エラトステネスの篩 を実装した eratosthenes.py を含むパッケージを作成してテストを行います。
カレントディレクトリの構成は以下になります。
(test)$ tree . . ├── setup.py ├── src │ ├── eratosthenes.py │ └── tests │ └── test_eratosthenes.py └── tox.ini
Python のパッケージングや仮想環境の作成方法については Python Hack-a-thon 4 ハンズオン 中級コース がとても参考になります。
setup.py の設定
tox を使ってテストするには Python のパッケージを作成しないといけません。
ここではテストすることが目的なので、パッケージ名とバージョンのみの設定をもつ setup スクリプトを記述します。
$ vi setup.py # -*- coding: utf-8 -*- from setuptools import setup setup( name="mytoxtest", version="0.1.0", )
tox.ini の設定
tox の設定ファイルは ini 形式で記述します。
$ vi tox.ini [tox] envlist = py26, py27, py32 [testenv] deps = pytest commands = py.test --doctest-module -v src/eratosthenes.py src/tests
[tox] セクションには、テスト対象の Python バージョンやワークディレクトリの位置などを設定します *3 。
[testenv] セクションには、依存するライブラリと実行するテストコマンドを記述しています。ここでは pytest を使っていますが、コマンドラインで実行できるものであれば何でも構いません。
pytest のおさらい
pytest で doctest とテストスクリプトを実行します。
commands = py.test --doctest-module -v src/eratosthenes.py src/tests
まずは指定した Python バージョンでテストを実行します。"-e" オプションを指定すると、envlist で指定した任意の Python バージョンでのみ実行します。試しに実行します。
(test)$ tox -e py27 _________________________________ [tox sdist] __________________________________ [TOX] ***creating sdist package [TOX] /Users/t2y/work/repo/misc/tox_test $ /Users/t2y/.virtualenvs/test/bin/python setup.py sdist --formats=zip --dist-dir .tox/dist >.tox/log/0.log [TOX] ***copying new sdistfile to '/Users/t2y/.tox/distshare/mytoxtest-0.1.0.zip' ______________________________ [tox testenv:py27] ______________________________ [TOX] ***reusing existing matching virtualenv py27 [TOX] ***upgrade-installing sdist [TOX] /Users/t2y/work/repo/misc/tox_test/.tox/py27/log $ ../bin/pip install --download-cache=/Users/t2y/work/repo/misc/tox_test/.tox/_download /Users/t2y/work/repo/misc/tox_test/.tox/dist/mytoxtest-0.1.0.zip -U --no-deps >5.log [TOX] /Users/t2y/work/repo/misc/tox_test $ .tox/py27/bin/py.test --doctest-module -v src/eratosthenes.py src/tests ============================= test session starts ============================== collected 173 items src/eratosthenes.py: [doctest] PASSED src/tests/test_eratosthenes.py: [doctest] PASSED src/tests/test_eratosthenes.py:21: test_sieve_of_eratosthenes[10-3] FAILED src/tests/test_eratosthenes.py:21: test_sieve_of_eratosthenes[30-10] PASSED src/tests/test_eratosthenes.py:21: test_sieve_of_eratosthenes[997-168] PASSED src/tests/test_eratosthenes.py:29: test_is_prime[2] PASSED src/tests/test_eratosthenes.py:29: test_is_prime[3] PASSED src/tests/test_eratosthenes.py:29: test_is_prime[5] PASSED ... (snip) src/tests/test_eratosthenes.py:29: test_is_prime[983] PASSED src/tests/test_eratosthenes.py:29: test_is_prime[991] PASSED src/tests/test_eratosthenes.py:29: test_is_prime[997] PASSED ================================= FAILURES ================================= _______________________ test_sieve_of_eratosthenes[10-3] _______________________ max_num = 10, expected = 3 @pytest.mark.parametrize(("max_num", "expected"), [ (10, 3), (30, 10), (PRIMES[-1], len(PRIMES)), ]) def test_sieve_of_eratosthenes(max_num, expected): > assert expected == len(list(sieve_of_eratosthenes(max_num))) E assert 3 == 4 E + where 4 = len([2, 3, 5, 7]) E + where [2, 3, 5, 7] = list(<generator object sieve_of_eratosthenes at 0x10185c7d0>) E + where <generator object sieve_of_eratosthenes at 0x10185c7d0> = sieve_of_eratosthenes(10) src/tests/test_eratosthenes.py:27: AssertionError ===================== 1 failed, 172 passed in 0.35 seconds ===================== [TOX] ERROR: InvocationError: '.tox/py27/bin/py.test --doctest-module -v src/eratosthenes.py src/tests' ________________________________ [tox summary] _________________________________ [TOX] ERROR: py27: commands failed
おっと、test_sieve_of_eratosthenes() のテストに失敗したようです。
pytest の結果レポートを表示させるためにあえて失敗させました。詳細は省きますが、len(list(...)) の内部処理が展開されていて、10までの素数は 2, 3, 5, 7 の 4 つ値が返り値だと分かります。このテストは、@pytest.mark.parametrize でパラメーター指定した値が誤っていました。
テストを修正して再実行します。全てパスすると "congratulations :)" と表示されます。
(test)$ tox -e py27 ________________________________ [tox sdist] _________________________________ [TOX] ***creating sdist package ... (snip) ========================= 173 passed in 0.34 seconds ========================= _______________________________ [tox summary] ________________________________ [TOX] py27: commands succeeded [TOX] congratulations :)
tox を使ってテストする
ある Python バージョンで成功したので、他の Python バージョンでもテストを実行してみましょう。"-e" オプションを指定せずに実行すると envlist で定義した全てのバージョンでテストが実行されます。
$ tox (test)$ tox ________________________________ [tox sdist] _________________________________ [TOX] ***creating sdist package [TOX] /Users/t2y/work/repo/misc/tox_test $ /Users/t2y/.virtualenvs/test/bin/python setup.py sdist --formats=zip --dist-dir .tox/dist >.tox/log/0.log [TOX] ***copying new sdistfile to '/Users/t2y/.tox/distshare/mytoxtest-0.1.0.zip' _____________________________ [tox testenv:py26] _____________________________ [TOX] ***reusing existing matching virtualenv py26 [TOX] ***upgrade-installing sdist ... ... (snip) ... _____________________________ [tox testenv:py27] _____________________________ [TOX] ***reusing existing matching virtualenv py27 [TOX] ***upgrade-installing sdist ... ... (snip) ... _____________________________ [tox testenv:py32] _____________________________ [TOX] ***reusing existing matching virtualenv py32 [TOX] ***upgrade-installing sdist ... ... (snip) ... _______________________________ [tox summary] ________________________________ [TOX] py26: commands succeeded [TOX] py27: commands succeeded [TOX] py32: commands succeeded [TOX] congratulations :) (test)$
Python 2.6, 2.7, 3.2 で全てのテストが成功しました、本当に簡単ですね o(^ ^)o
インデックスサーバーの切り替え
tox.ini で indexserver を指定して、任意の pypi サーバーを設定できます。
[tox] envlist = py26, py27, py32 indexserver = testrun = http://pypi.testrun.org pypi = http://pypi.python.org/simple [testenv] deps = :testrun:pytest :pypi:sphinx
pytest や tox の tox.ini を見ると、http://pypi.python.org/ に加えて http://pypi.testrun.org/ も設定されていました。ローカルの pypi ミラーサーバー、もしくは開発版のパッケージを置いておくサーバーなどに活用できます。ローカルに pypi サーバーを構築する方法はまた別の機会に書きます。
まとめ
tox は本当に簡単で便利なツールです。
Python 3 未対応のパッケージに出会ったとき、tox.ini を追加してテスト環境を構築できれば、互換性を保持した上で Python 3 対応を行う敷居が格段に低くなります。さらに pytest の結果レポートの分かりやすさと組み合わせて、失敗するテストを直すのがおもしろくなってきたりします。また tox と pytest 共に開発者が Holger Krekel 氏なので、今後もお互いの親和性を保って拡張されていくように期待できます。
pytest と tox を使って Python 3 対応を本格的に始めてみてはどうでしょうか。
データ駆動テストを nose と pytest でやってみた
pytest で初めてテストを書いてみました。
今度こそ帰るー、py.test を使って初めてテストを書いた、評判通りすごく良い
@t2y noseと比べた感想とか聴きたいです。
2012-02-07 19:55:04 via web to @t2y
@methane @t2y テストがこけたときまともなレポートをはくのがpy.testのいいところ
2012-02-07 19:56:34 via twicca to @methane
nose と比べて、データ駆動テスト *1 *2 の違いが大きかったのでまとめてみます。
準備
以下の素数判定を行うテスト対象関数があるとします。
PRIME = {2: True, 3: True, 4: False, 5: True, 6: False, 7: True} def is_prime(num): return PRIME[num]
bitbucket にこの記事で紹介するテストコードを置きました。興味のある方は試してみてください。
Python 2.7 でテストしています。
(test)$ pip freeze distribute==0.6.24 nose==1.1.2 py==1.4.7 pytest==2.2.3 virtualenv==1.7 wsgiref==0.1.2
最も簡単なデータ駆動テスト
nose でデータ駆動テストを行う場合、ジェネレーターでテストケースを生成します *3 。以下のようにループを使ったテストコードになります。
from nose.tools import ok_ def test_is_prime(): for num in [3, 4, 5]: yield ok_, is_prime(num)
実行結果。
(test)$ nosetests -v test_nose-data-driven.py test_nose-data-driven.test_is_prime(True,) ... ok test_nose-data-driven.test_is_prime(False,) ... FAIL test_nose-data-driven.test_is_prime(True,) ... ok ====================================================================== FAIL: test_nose-data-driven.test_is_prime(False,) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/case.py", line 197, in runTest self.test(*self.arg) File "/Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/tools.py", line 25, in ok_ assert expr, msg AssertionError ---------------------------------------------------------------------- Ran 3 tests in 0.001s FAILED (failures=1)
3, 4, 5 という入力データに対して、num = 4 のときにテストが失敗します。num = 4 のときにテストが失敗しても 3 と 5 のテストは実行されているので、一回の実行で全ての入力データに対するテストが行えて便利です。もっとたくさんの入力データを取るテストの場合、何度も実行する手間隙が省けます。
しかし、テストが失敗したとき、この結果レポートでは、テストコードのどこで、どんな入力データに対して失敗したのか分かりません。パッと見て何のエラーなのかよく分かりません。
ここで pytest でも同じことをやってみます。
pytest でデータ駆動テストを行う場合、デコレーターで入力データを与えます *4 。ループを書かなくて良いのでテストコードもすっきりしますね。
import pytest @pytest.mark.parametrize("num", [3, 4, 5]) def test_is_prime(num): assert is_prime(num)
実行結果。
(test)$ py.test -v test_pytest-data-driven.py ============================= test session starts ============================== test_pytest-data-driven.py:10: test_is_prime[3] PASSED test_pytest-data-driven.py:10: test_is_prime[4] FAILED test_pytest-data-driven.py:10: test_is_prime[5] PASSED ================================= FAILURES ================================= _______________________________ test_is_prime[4] _______________________________ num = 4 @pytest.mark.parametrize("num", [3, 4, 5]) def test_is_prime(num): > assert is_prime(num) E assert is_prime(4) test_pytest-data-driven.py:12: AssertionError ====================== 1 failed, 2 passed in 0.02 seconds ======================
nose の結果レポートと比べると、驚くほど懇切丁寧なレポートです。テストコードの、どこで、どんな入力データに対してテストが失敗したのか一目瞭然です。
この結果レポートの違いを見るだけでも pytest を使う価値があります。
デバッガを使う
pytest はデコレーターで入力データを渡せましたが、この違いはデバッグのやり方にも影響します。nose と pytest 共にテストが失敗したときに pdb デバッガを起動するオプションがあります。
まずは nose でテストが失敗したときにデバッガを起動します。"--pdb-failure" オプションを指定します。
(test)$ nosetests -v --pdb-failure test_nose-data-driven.py test_nose-data-driven.test_is_prime(True,) ... ok test_nose-data-driven.test_is_prime(False,) ... > /Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/tools.py(25)ok_() -> assert expr, msg (Pdb) locals() {'msg': None, 'expr': False} (Pdb) w /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/case.py(327)run() -> testMethod() /Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/case.py(197)runTest() -> self.test(*self.arg) > /Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/tools.py(25)ok_() -> assert expr, msg (Pdb) u > /Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/case.py(197)runTest() -> self.test(*self.arg) (Pdb) locals() {'self': test_nose-data-driven.test_is_prime(False,)} (Pdb) u > /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/case.py(327)run() -> testMethod() (Pdb) locals() {'self': test_nose-data-driven.test_is_prime(False,), 'orig_result': <nose.result.TextTestResult run=2 errors=0 failures=0>, 'testMethod': <bound method FunctionTestCase.runTest of test_nose-data-driven.test_is_prime(False,)>, 'success': False, 'result': <nose.result.TextTestResult run=2 errors=0 failures=0>}
デバッガを起動したものの、これは test_is_prime() のコンテキストではありません。そのため、入力値 (num) の情報もありません。
次に pytest でテストが失敗したときにデバッガを起動します。"--pdb" オプションを指定します。
(test)$ py.test -v --pdb test_pytest-data-driven.py ... (snip) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> > /Users/t2y/work/repo/misc/data_driven_test/test_pytest-data-driven.py(12)test_is_prime() -> assert is_prime(num) (Pdb) locals() {'@py_format4': 'assert False\n{False = is_prime(4)\n}', 'num': 4, '@py_assert2': False} (Pdb) w ... (snip) > /Users/t2y/work/repo/misc/data_driven_test/test_pytest-data-driven.py(12)test_is_prime() -> assert is_prime(num)
test_is_prime() のコンテキストであることから 'num': 4 が確認できました。
より複雑なテストで失敗したとして、テスト関数のコンテキストにおける情報をみたいときに
import pdb; pdb.set_trace()
のようにテストコードを修正しなくても pytest なら "--pdb" オプションを指定してデバッグできます。
やはり pytest 良いですね。
テストフィクスチャを使ったデータ駆動テスト
wikipedia:XUnit スタイルの setup/teardown を使ったデータ駆動テストもやってみます。
nose も pytest もクラス内の setup()/teardown() メソッドを認識して、テスト関数の前後で実行してくれます *5 *6 。テストコード内の print() で出力したメッセージを表示するには、どちらも "-s" オプションを指定します。
実行順序を分かりやすくするためにデバッグメッセージを出力するように変更します。
def is_prime(num): print("called is_prime: {0}".format(num)) return PRIME[num]
まずは pytest からテストします。
class TestPrimeNumber(object): def setup(self): print("\ncalled setup") def teardown(self): print("\ncalled teardown") @pytest.mark.parametrize("num", [3, 4, 5]) def test_is_prime(self, num): assert is_prime(num) def test_function(self): print("called test function") assert True
実行結果。
(test)$ py.test -v -s test_pytest-data-driven.py ============================= test session starts ============================== collected 4 items test_pytest-data-driven.py:23: TestPrimeNumber.test_is_prime[3] called setup called is_prime: 3 PASSED called teardown test_pytest-data-driven.py:23: TestPrimeNumber.test_is_prime[4] called setup called is_prime: 4 FAILED called teardown test_pytest-data-driven.py:23: TestPrimeNumber.test_is_prime[5] called setup called is_prime: 5 PASSED called teardown test_pytest-data-driven.py:27: TestPrimeNumber.test_function called setup called test function PASSED called teardown ================================= FAILURES ================================= _______________________ TestPrimeNumber.test_is_prime[4] _______________________ self = <test_pytest-data-driven.TestPrimeNumber object at 0x1017ddc50>, num = 4 @pytest.mark.parametrize("num", [3, 4, 5]) def test_is_prime(self, num): > assert is_prime(num) E assert is_prime(4) test_pytest-data-driven.py:25: AssertionError ====================== 1 failed, 3 passed in 0.02 seconds ======================
前節の普通の関数と同じ感覚でテストが記述できて、その結果レポートも期待したものが表示されます。良いですね。
次に nose でテストします。
class TestPrimeNumber(object): def setup(self): print("\ncalled setup") def teardown(self): print("called teardown") def test_is_prime(self): for num in [3, 4, 5]: yield ok_, is_prime(num) def test_function(self): print("called test function") ok_(True)
やはり先ほどと同様に記述して、一見テストも実行できるのですが、、、
(test)$ nosetests -v -s test_nose-data-driven.py ... (snip) test_nose-data-driven.TestPrimeNumber.test_function ... called setup called test function called teardown ok called is_prime: 3 test_nose-data-driven.TestPrimeNumber.test_is_prime(True,) ... called setup called teardown ok ... (snip)
is_prime() が setup() の前に実行されています。このテストコードは意図した順番で実行されません。昔、これではまりました (> <)
nose のテストジェネレーターはテストケースを生成するものなので、正しくは以下のように記述します。
def test_factory(): class FactoryTestCase(object): def __init__(self, num): self.num = num def __call__(self): ok_(is_prime(self.num)) def setup(self): print("\ncalled setup") def teardown(self): print("called teardown") for num in [3, 4, 5]: yield FactoryTestCase(num)
実行結果。
(test)$ nosetests -v -s test_nose-data-driven.py test_nose-data-driven.test_factory ... called setup called is_prime: 3 called teardown ok test_nose-data-driven.test_factory ... called setup called is_prime: 4 FAIL called teardown test_nose-data-driven.test_factory ... called setup called is_prime: 5 called teardown ok ====================================================================== FAIL: test_nose-data-driven.test_factory ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/case.py", line 197, in runTest self.test(*self.arg) File "/Users/t2y/work/repo/misc/data_driven_test/test_nose-data-driven.py", line 41, in __call__ ok_(is_prime(self.num)) File "/Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/tools.py", line 25, in ok_ assert expr, msg AssertionError ---------------------------------------------------------------------- Ran 3 tests in 0.001s FAILED (failures=1)
何となくテスト関数が少し分かり難くなった気がします。
さらに補足として、テストジェネレーターは unittest.TestCase のサブクラスでは動作しません。
関数単位のテストフィクスチャを使ったデータ駆動テスト
前節では、テスト関数の入力データに対するそれぞれの呼び出し毎に setup()/teardown() が呼ばれていました。似て非なる例として、テスト関数に対して一回だけ呼び出すテストをやってみます。
nose だと関数に対する with_setup() デコレーターで指定します。テストジェネレーターで実行するときは、やはり FactoryTestCase を定義しないといけません。
from nose.tools import with_setup def mysetup(): print("called mysetup") def myteardown(): print("called myteardown") @with_setup(mysetup, myteardown) def test_is_prime_with_setup_teardown(): class FactoryTestCase(object): def __init__(self, num): self.num = num def __call__(self): ok_(is_prime(self.num)) for num in [3, 4, 5]: yield FactoryTestCase(num)
実行結果。
(test)$ nosetests -v -s test_nose-data-driven.py called mysetup test_nose-data-driven.test_is_prime_with_setup_teardown ... called is_prime: 3 ok test_nose-data-driven.test_is_prime_with_setup_teardown ... called is_prime: 4 FAIL test_nose-data-driven.test_is_prime_with_setup_teardown ... called is_prime: 5 ok called myteardown ... (snip)
mysetup()/myteardown() が一回だけ呼ばれていますね。
次に pytest です。pytest は、もう少し汎用的な仕組みで、テストモジュール内の、全ての関数に対してフックする setup_function()/teardown_function() を定義する方法があります。
これまでの例と違う点として、このテストは pytest.mark.parametrize に入力値のリストを渡して、テスト関数内にループを記述しています。
def setup_function(function): print("\ncalled setup: {0}".format(function)) def teardown_function(function): print("\ncalled teardown: {0}".format(function)) @pytest.mark.parametrize("nums", ([3, 4, 5],)) def test_is_prime2(nums): for num in nums: assert is_prime(num)
実行結果。
(test)$ py.test -v -s test_pytest-data-driven.py ============================= test session starts ============================== platform darwin -- Python 2.7.2 -- pytest-2.2.3 -- /Users/t2y/.virtualenvs/test/bin/python collected 1 items test_pytest-data-driven.py:39: test_is_prime_with_setup_teardown[.0] called setup: <function test_is_prime_with_setup_teardown at 0x1017d0758> called is_prime: 3 called is_prime: 4 FAILED called teardown: <function test_is_prime_with_setup_teardown at 0x1017d0758> ... (snip)
実行できましたが、このテスト方法はループ内に assert 文を記述しているので num = 4 で失敗するとテストが終了します。さらに setup_function()/teardown_function() は全ての関数に対してフックしてしまうので、使い勝手が悪いかもしれません。
もう1つの方法として、テスト関数の引数に対してフックする方法があります *7 *8 。これは nose にはない仕組みで、ちょっと驚きました。
def mysetup(): print("\ncalled mysetup") return [3, 4, 5] def myteardown(nums): print("\ncalled myteardown: {0}".format(nums)) def pytest_funcarg__nums(request): return request.cached_setup(setup=mysetup, teardown=myteardown) def test_is_prime_funcarg_setup_teardown(nums): for num in nums: assert is_prime(num)
"nums" という引数に対してフックする pytest_funcarg__nums() を定義して、その中で任意の setup/teardown 関数を指定します。そして、mysetup() がテスト関数の入力データになる "nums" を返します。
実行結果は先ほど同じです。こちらの方が任意の関数に対しては使いやすそうですが、テストコードが分かり難くなってしまうので使い方は限定されるように思います。
(test)$ py.test -v -s test_pytest-data-driven.py ... (snip) test_pytest-data-driven.py:55: test_is_prime_funcarg_setup_teardown called mysetup called is_prime: 3 called is_prime: 4 FAILED called myteardown: [3, 4, 5]
まとめ
データ駆動テストを nose と pytest で行うときの違いをまとめました。
複数の入力データを与えるテストを行う場合、どちらのライブラリも機能的には同じようにテストできますが、結果レポートの分かりやすさ・デバッグのしやすさを考慮すると pytest の方が使い勝手が良いと私は思いました。
あわせてやってみよう。
追記
ちょっと勘違いしたみたい。
@imagawa_yakata @t2y さんのBlogのコードでは、 yield ok_, is_prime(num) の時点で is_prime が呼び出されているので、 yield is_prime, num じゃないとダメですね。
2012-02-15 02:14:38 via web to @imagawa_yakata
*1:UxUを用いたデータ駆動テストの記述 - ククログ(2009-10-30)
*2:Python用単体テストフレームワークPikzie 0.9.7リリース - ククログ(2010-05-25)
*4:pytest: Simple "decorator" parametrization of a test function
*6:pytest: Extended xUnit style setup fixtures
*7:pytest: Injecting objects into test functions (funcargs)
*8:http://anders.conbere.org/blog/2011/05/03/setup_and_teardown_methods_with_py.test/
アリエル・ネットワークでアルバイトをしてきました
3ヶ月半という短い期間でしたが、アリエル・ネットワーク (以下アリエル) でプログラマーとしてアルバイトしてきました。
よく見かける、どこそこでインターンシップをしてきました風な記事を書いてみます。普通はそういった記事を学生さんが書いているものですが、この記事は普通の無職の人が書いています。
最終日に「開発方法論提案 改めアリエル開発の所感」というタイトルで発表しました。
私が過去にいくつかの会社で働いてきた中で、アリエル開発で改善したら良いと思うことがあったら提案してほしいと言われていました。とはいえ、私の経験よりもアリエル開発の方がずっとレベルが高かったため、釈迦に説法な気がして、そんなタイトルに落ち着きました。この記事では、その内容からいくつか抜き出して紹介します。
私がやったこと
アリエル・エンタープライズ という Web アプリなグループウェアの開発に携わりました。
Trac でアサインされたチケット (バグや機能拡張) に対して修正するといった感じです。4-5年ぐらい開発を継続しているらしく、製品としての中核や部品、UI はかなり作り込まれていました。そのため、自分でコードをガリガリ書くというよりは、製品の動作や仕組みを理解するためにコードを読んで原因を特定したり、既存のコードを再利用して機能拡張するといった開発作業がほとんどでした。
製品の規模もコード量が数十万行というオーダーの、私にとってはこれまでで最も大規模なアプリケーションでした。開発を通して、大規模アプリの設計、ソースコードの読み方、Java のイディオムも少しずつ分かってきておもしろかったです。
アリエルの開発は、プロダクト系の開発だと Java、UI 系の開発だと Javascript がメインのようです。ただプロダクトと連携するツール類は Ruby や Python で書かれたものもいくつかありました。なので、業務で Java か Javascript しか書けないというわけでもありません。試験的な取り組み、プロトタイプ的なツールなどは、開発者の好みの言語で実装できそうに思えました。実際、私自身も Python でプロダクトと連携するツールを実装しました。
アリエル開発の良かったこと
開発体制/インフラ
雑誌や書籍で見かける類いの開発手法を当たり前のように業務の中に取り入れてました。開発インフラが整っていると、開発者はこんなに楽なんだということを実感しました。
また、開発者にとっての業務の中心は Trac です。チケット管理、wiki、ドキュメント、レポーティング (グラフ)、技術メモなど、かなり活用されてました。会社として開発情報を統合的に一元管理しようという意図をもって Trac に集約していて良い考えだなと思いました。
そんな中、チケットが分裂・派生していき、手動で関連チケットのリンクを張るのが煩わしくなって TracTicketReferencePlugin も作ってみました。これはアリエルの 10% ルールという枠組みの中で開発したものです。アリエルの Trac で耐えたら、世の中では大丈夫だろうと個人的に思っています。
仕様の決め方
アリエルの開発における仕様の決め方に、最初は戸惑ったのですが、これはこれで良いこともありそうだと後になって考え直した次第です。緩い感じに仕様を策定していきます。
バグ修正的なチケットは、バグを直せばいいだけです。大きな機能追加は全く違った開発プロセスを歩むので、ここでは書きません。適切な粒度の機能追加的なチケットは、大まかな方向性は決まっていますが、詳細は決まっていません。基本は担当開発者がまず、外部仕様策定から始めます。仕様は、チケットをあげたヒトやプロジェクトマネージャ、プロダクトマネージャと相談しながら決まっていきます。プロダクトマネージャがこうしたいと思っても、表向きは担当開発者が決める形になっています。ただ、あまりにも紛糾していて物事が進まないときは、偉いヒトの強権が発動されます。
http://blog.pasonatech.co.jp/ootani/105/17869.html
然るべき人が、ちゃんとした仕様を決めて、一方的に押し付けるというよりは、そのチケットの関係者が議論しながらより良い仕様に作り上げていくといった感じです。この過程を厳密にシステム化、もしくはプロセス化しないことで関係者間のコミュニケーションを促進している面もあると私は思いました。困ったら関係者のところへ行って「これってどういうことですか?」と聞くしかありません。そこで要件の齟齬や考え方の違いを認識して、やり取りから開発者も学ぶ、視野が広がるといったことに繋がるように思いました。
組織的な牽制機構
その緩い仕様の決め方にはもう1つのメリットもあります。
組織的な牽制機構が働かないと、ほとんどの組織は堕落します。
組織が1つだけだと、なぁなぁになってしまいます。組織が2つだと、けんかして対立関係になりがちです。組織が3つで、相互に牽制する関係が私の開発経験の中では、結果的にうまくいっていたように思います。アリエルもそういった3つ巴的な体制になっていて良さそうに私は思いました。但し、組織が分かれると、情報共有されなかったり、生産性が落ちたりするので、横の連携の工夫が必要になります。
会議がなかった
私がアルバイトだったこともあるかもしれませんが、会議がほとんどありません。こんなに会議がない会社も初めてでした。週報も書かないので、週例ミーティングがなく、グループミーティングのようなものもありませんでした。意図的に会議を減らしているように思いますが、開発に集中してたら1日中全く話さなくて終わるような日もあったかもしれません。
しかし、月に1回の開発部の全体会議がいまいちでした。私からの唯一の提案は、どうせやるならちゃんと全体会議をやった方が良いですという当たり前の意見を述べてみました。
また、Trac の活用度が高いため、開発の進捗状況やいろんな数字が出せるので、そういった数字からビジョンを語ると良いのではないかと偉そうなことを言ってみました。実際に Trac 上ではロードマップを管理して、バーンダウン・チャートも出力されていました。
パッケージ開発は長期的な展望をもって開発するので、普通の開発者は何を目指して開発しているのか、いまどういう状況なのかがよく分からなくなることがあります。何かしら開発者の意思統一を図るというか、共有するためにビジョンが必要です。ビジョンを語るのはとても難しいことですが、かっこいいビジョンばかり語っても本当の意味ではよく分からないので、実際の数字から現状分析や今後の方向性を語ると分かりやすくて良いのではないかと私は思います。
まとめ
・・・
この記事そのものは特に何かを伝えたいわけでもないのですが、アリエル開発の雰囲気が少しでも伝われば良い、、、のかな。
発表後に CTO がやってきて「その資料、公開しても良いです。公開するしないの判断は任せます」という謎なコメントを残して去っていきました *1 。そのまま公開しても中の人でないと、半分ぐらいは意味が分からないので補足を加えながら、自分の考えも整理してみました。
アリエルの文化はアドベントカレンダーを辿ってみると、おもしろいと思います。
開発者も募集してるようです
私から見ると、開発者にとっては居心地の良い開発環境、職場だと思います。いやな人がいない。
まずはアルバイトからも受け入れているようです。興味のある方は応募してみると良いと思います *2 。
*1:勝手にクックパッド vs. アリエル | ありえるえりあ
*2:アリエル・ネットワーク社の選考や採用に私は全く関係ありません
イテレーターという抽象概念にもの思い
元ネタ:
- map_between をやってみる - Study08.net 対シンバシ殲滅用人型機動兵器
- http://d.hatena.ne.jp/imagawa_yakata/20120105/1325781387
最近、Java のコードばかり書いています。
Java には、Python にはない wikipedia:抽象型 としてのインターフェースがあります。
パーフェクト Java から引用すると、インターフェースは、
オブジェクトにそのインターフェースの決めた振る舞いを期待することの表明
だと説明されています。また Java でプログラミングする上で、インターフェースを意識した抽象化が、複雑さの軽減、保守しやすいコードを書くのに重要だとあります *1 。
以前、EuroPython 2011 のイベントレポートを執筆した際に *2 、私はイテレーターという用語の扱い方に何となく腑に落ちていませんでした。Python では抽象クラスやインターフェースが言語機能として提供されていません。そのとき @tk0miya に「イテレーターはインターフェースだと考えたら良い」と教えてもらって、分かったような分かってないような日々を過ごしていました。
wikipedia の定義によると、
イテレータ (Iterator) とは、プログラミング言語において配列やそれに類似するデータ構造の各要素に対する繰返し処理の抽象化である。
イテレータ - Wikipedia
とあります。イテレーターと聞いて Python で言う iterable なオブジェクトであるリストやタプルをイメージしてしまっていたのが理解を妨げていました。
また Java の話しに戻ります。
最近、Java のコードばかり、、、あっ、違う。
閑話休題。Java5 から利用できる拡張 for 文という構文を使うと、Python の for in 文と同じような感覚でイテレーターを扱えます。例えば、インターフェース java.util.List の定義を調べてみます。すると、java.util.Iterator を返すインターフェース java.util.Iterable を継承していることが分かります。UML で書くと、こんな感じです (たぶん) 。
Java の Iterable インターフェースの iterator() メソッドが、Python でイテレーター型をサポートするための __iter__() メソッド、Iterator インターフェースの next() メソッドが同じく Python で言う next() メソッドに相当します *3 。つまり、どちらの言語もほとんど同じような仕組みになっているわけです。
java.util.List で宣言したオブジェクトであれば、イテレーターのインターフェースを備えている (と期待できる) ので、どんなデータ型であっても拡張 for 文で繰り返し処理できます。言い換えれば、Java.util.List と宣言することが、繰り返し処理できることの表明であり、プログラマへもその意図を伝えられます。
インターフェースにより、型の違いや実装の詳細を意識させずに抽象化できることが Java におけるパラダイムの1つなんだと少し分かってきました。つまり、あるデータ型のオブジェクトが与えられたとき、
List<Item> data = new myData();
data オブジェクトの詳細を知らなくても iterable なオブジェクトだと分かります。もちろん Python でも似たようなことはできます。
class MyIterable(object): def __iter__(self): return self def next(self): return self.next() class MyData(MyIterable): ... data = MyData();
この場合、継承している MyIterable クラスの実装を覗き見ることで、あぁこれは iterable なオブジェクトなんだと分かるわけですね。
Java はイテレーターという抽象化された概念とその実装がインターフェースを使うことで明確に分離されています。しかし、Python の場合、分離されるかどうかは実装依存になってしまいます。MyIterable クラスを定義せずに直接、MyData クラスに __iter__() と next() メソッドを実装しても構いません。このことが私にとっては、イテレーターと呼び方を変えても、結局のところはリストやタプルのようなオブジェクトのことなんでしょ、、、といった、抽象概念としてのイテレーターと iterable なオブジェクトとの違いを混同して考えてしまう要因となっていました。
Python でプログラミングするだけなら、混同して考えてもそう害はないのかな?という気もしますが、プログラミング一般の概念として他の人と話したときにちょっと違和感を感じたものの正体がようやく分かってきた気がします。また Java を勉強していたら Python の特徴をより理解した気持ちになってちょっと嬉しかったです。
まとめると、最近、Java のコードばかり書いているんです。
パーフェクトJava (PERFECT SERIES) (PERFECT SERIES 2)
- 作者: アリエル・ネットワーク株式会社,井上誠一郎,永井雅人,松山智大
- 出版社/メーカー: 技術評論社
- 発売日: 2009/09/24
- メディア: 大型本
- 購入: 26人 クリック: 360回
- この商品を含むブログ (35件) を見る
本当の本当は誰が怖い!?
好評につき続編です。はい、嘘です。
先日、 本家Ariel Advent Calendar 2011 *1 の記事を書きましたが、今回は 元祖Ariel Advent Calendar 2011 *2 を書きます。このブログに読者という方々がいるならば、一体何をやってるんだろうか?と首を傾げたくなるでしょうが、大丈夫です。私自身、その意味も意図も、理由すら分からずに書いています。
今北産業
なぜか CTO が本家からあぶれてしまい、元祖を立ち上げて所謂、骨肉の争いに突入しました。
そして、ダークサイドに落ちたありえるたんの *3 、闇えるたん (@ArieLeaks) も現れ、三竦みの陣容を取っています。
私は日々、3強の影に怯えながら、ひっそり開発を継続しています。
番長の暗躍
なるべく私は目立たないように開発していますが、たまに番長に見つかってしまうときがあります。
これは Trac のチケット管理のやり取りです。
リグレッションを起こした開発者への指導
私がある機能拡張を行った際に、そのスキーマ定義が完全ではなくて、別の箇所でリグレッションを起こしてしまいました。一旦は、私宛に担当を割り振ったものの、私が直す前に番長に修正されてしまいました。
つまらんミスをしやがって。もうお前にコードは触らせない。
と、言われているようなものです。
闇えるたんの胎動
行動がまったく読めません。
ぷっ、笑っちゃうよね。#アリエルCTO 元祖Ariel Advent Calendar 2011 URL
@arielnetwork #アリエルCTO をガンジス川に蹴り落としてやったわよ。今頃はワニの餌ね。
2011-12-21 10:50:51 via web to @arielnetwork
インドへ旅立った CTO を追いかけて行って闘いを繰り広げているようです。
闇えるたんよ。覚悟なさい | ありえるえりあ によると、会社ブログもクラックされたようです。
CTO との軋轢
もちろん CTO と社員との諍いも絶えません。
悪のりするときは、とことん悪のりしたら良い。
という総帥の教えに従います。
もともとアドベントカレンダーのイメージはこうでした *4 。
CTO がインドへ行った間隙を突いてコラージュされました。
もうやりたい放題です。
CTO は、きっと反攻の策を練っていることだと思います。
最近は #アリエルCTO をからかうと、「インドに飛ばすぞ!おりゃ」って脅されます。CTOは怖いです
組長の真実
どうやら組長と呼ばれる人もいるそうです、まぁ、私なんですが。
一説には組長も怖いという風評があるようですが、それは明らかな間違いです。
@t2y 遅刻してすみません、、お布団から出れませんでした (´・ω・`)
2011-12-14 09:19:45 via Silver Bird to @t2y
@kawanoshinobu のんびりすると良いと思います、そんな日もあるものです、遅刻しても怖いことは何もありません
組長は、ただ朝早いだけで人畜無害です。朝早くきて、ひっそりと開発に明け暮れてるだけのようです。
まとめ
かなり内輪感の強い内容を紹介しました。
開発というお仕事は、時にどうして良いか分からなくて不安に陥ったりします。世の中、本当に怖いことはたくさんあります。どんな状況であっても、心にゆとりをもつこと、ユーモアを受け入れること、おもしろいと思えることが大事です。
アリエルという会社の、そんな雰囲気が伝われば良いなと思います。
pyrtm の Python 3 対応
(第14回)Python mini Hack-a-thon - connpass に参加してきました。
Python で Remember The Milk API を扱う pyrtm 0.4 をリリースしました *1 。
やったこと
Python 2/3 両対応です。
たまたま Python3 対応の Pull リクエスト が来ていました。やろうと思えば、いつでもできる類いの修正ですが、こういうリクエストが来るということそのものが嬉しいですね。リクエストをもらったことをきっかけに取り組みました。
Pull リクエストの内容は、2to3 をベースに Python 3 で動作するようにした修正でした。Python 2/3 両対応するにあたって、
の2通りのやり方があります。周りの開発者にどっちが良いのさ?と聞いてみたら後者の方を支持する意見が多かったのでそちらで対応しました。
Python 3 対応については ライブラリをPython3対応に書き換える がとても参考になります。
実際に pyrtm でやってみて遭遇した 2 と 3 の違いをいくつか取り上げます。Python 2.6 は Python 3 の構文をサポートしているとは言っても、何かしらエラーが起きることもあるので、ちゃんと両方でテストする必要がありそうです。
- ライブラリ名/構造の違いによるインポート系の修正
try: from urllib.request import urlopen from urllib.parse import urlencode except ImportError: from urllib import urlencode, urlopen
こういうのはどうしようもないんですよね?
- print 関数への file 引数
- print('Usage: rtm_appsample APIKEY SECRET [TOKEN]', file=sys.stderr) + sys.stderr.write('Usage: rtm_appsample APIKEY SECRET [TOKEN]\n')
sys.stderr じゃないといけない理由はないのですが、オリジナルのコードを尊重してみました。
- sort メソッドの key 引数
- keys.sort()
+ keys.sort(key=str)
Python 3 だと、異なるオブジェクトが入ったリストで sort() メソッドを呼び出すと TypeError になります。key 引数で比較関数へ渡すキーの変換方法を明示する必要があるようです。
>>> [1, "a"].sort() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unorderable types: str() < int()
まとめ
Mac OS X と Fedora15 の Python 2.6 と 3.2 でテストしました。pyrtm ぐらいのツールなら、対応そのものは難しくありませんが、テストするのがちょっと面倒ですね。