Eric Sink on the Business of Software 革新的ソフトウェア企業の作り方

誰かの書評を読んでウィッシュリストに入れていたのを、ある時にお祝いとして id:rokujyouhitoma から頂きました。読むのが今頃になってしまいましたが、ありがとうございます!

しかし、いまとなっては、誰の書評を読んだのか、なぜ私は本書をウィッシュリストに入れたのか全く思い出せません (> <)、それでも id:rokujyouhitoma には感謝しています!

積んどく状態でしたが、ふと読み始めたらおもしろかったです。読みやすく、分かりやすく、統合性があって良い書籍だと思います。

小さく営むための知恵

著者の言う「小さな ISV」とは、いわゆるパッケージベンダーです。

ISV は作りたい製品を構想し、それを作り上げた時になってもまだ欲しがる人がいることに賭け、リスクを取る。ソフトウェア製品を持たない会社は ISV ではない。「小さな ISV」というのは ISV の大きくないもののことだ。

小さな ISV は小さいままでいる傾向がある。大きくなるにしても、成長はゆっくりしている。有機的に成長し、自身の利益を投資することで成長する。小さな ISV は地味でも収益を上げていることが多い。

執筆された時期が 2003-2005 年の記事が多いので、やや古い内容もありますが、その知恵は時代や状況が変わっても応用できそうです。考え方の要点や注意を向ける視点について、経営資源の「ヒト」「モノ」「カネ」に対して、小さな ISV ならではの、フットワークの軽さを考慮して書かれています。

ここで「小さな」というのが重要な点で、大きな組織と小さな組織ではやり方が異なるというのを認識するのにも適した内容です。

ギークのためのファイナンス入門

本書の特徴の1つとして、ソフトウェア開発者の視点から企業や業務活動全般を俯瞰した内容を網羅している点です。

例えば、私にとっては「第4章 ギークのためのファイナンス入門」が、財務の専門的な用語や仕組みの理解を必要とせず、要点のみをシンプルに説明したもので分かりやすかったです。もちろん著者も本章を財務上のアドバイスとして受け取るべきではなく、ファイナンスの専門家に相談する必要があると説いています。ただ、ファイナンスの専門家のアドバイスも必ずしも全てが適切であるとは限らないということです。

戦略的意思決定にかかわる財務諸表として、以下の3つは読めるようになった方が良いと奨めています。

開発者が財務のことを知る必要はないという意見もあるかもしれませんが、私はある程度分かった方が良いと考えています。というのは、当たり前の話しですが、ビジネスが成り立たないと開発を継続できません。開発を継続できないと、技術的な挑戦や自身のキャリアを磨くこともできません。このプロジェクト (企業) は先細りだなとか、数年後に潰れそうだなとか、開発者も自ら判断して取捨選択していくことが重要になっていくように思います。

私がこれまで働いた企業だと、経営者は賃借対照表のみを説明したがります。しかし、パッケージベンダーの賃借対照表は全く当てにならないです。ソフトウェアは、無形固定資産や棚卸資産に含めて、それなりに数字を誤摩化せるからです。実際、ある程度は仕方ないのでしょうが、そのバランスはどのぐらいが適性な状態かを開発者が理解することは難しいと思います。

また、売上総利益率という指標から、その高低でビジネスモデルの違いを述べ、さらにオープンソースビジネスモデルが難しい理由を売上総利益率が低いからと説明しているのはおもしろい視点でした。

Just Do It

おそらく最も一般的な誤りは、ソフトウェア製品を作りたいと思いながら、それについて何もしないということだろう。

エピローグが (私にとっては) 最もユーモアのある、ちょっと良いお話でした。

アイディアは誰でも思い付くし、それが売れるものかどうかなんて、やってみないと分からない。小さなアイディアを小さく始めて、失敗したらまた別の小さなアイディアを始めれば良いだけの話しである。

結論: 私は2週間の注意深い作業をして「今を生きよ」という以上のことを何も言っていないアーティクルを書くことになるだろう。
そんなのは馬鹿げている。

Eric Sink on the Business of Software 革新的ソフトウェア企業の作り方

Eric Sink on the Business of Software 革新的ソフトウェア企業の作り方

tox と pytest で Python 2/3 両対応のテストを実行する

pytest *1 に関連して tox も一緒に覚えておくと良さそうです。


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 で初めてテストを書いてみました。

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 の方が使い勝手が良いと私は思いました。

あわせてやってみよう。

追記

ちょっと勘違いしたみたい。

アリエル・ネットワークでアルバイトをしてきました

3ヶ月半という短い期間でしたが、アリエル・ネットワーク (以下アリエル) でプログラマーとしてアルバイトしてきました。

よく見かける、どこそこでインターンシップをしてきました風な記事を書いてみます。普通はそういった記事を学生さんが書いているものですが、この記事は普通の無職の人が書いています。

最終日に「開発方法論提案 改めアリエル開発の所感」というタイトルで発表しました。

私が過去にいくつかの会社で働いてきた中で、アリエル開発で改善したら良いと思うことがあったら提案してほしいと言われていました。とはいえ、私の経験よりもアリエル開発の方がずっとレベルが高かったため、釈迦に説法な気がして、そんなタイトルに落ち着きました。この記事では、その内容からいくつか抜き出して紹介します。

私がやったこと

アリエル・エンタープライズ という Web アプリなグループウェアの開発に携わりました。

Trac でアサインされたチケット (バグや機能拡張) に対して修正するといった感じです。4-5年ぐらい開発を継続しているらしく、製品としての中核や部品、UI はかなり作り込まれていました。そのため、自分でコードをガリガリ書くというよりは、製品の動作や仕組みを理解するためにコードを読んで原因を特定したり、既存のコードを再利用して機能拡張するといった開発作業がほとんどでした。

製品の規模もコード量が数十万行というオーダーの、私にとってはこれまでで最も大規模なアプリケーションでした。開発を通して、大規模アプリの設計、ソースコードの読み方、Java のイディオムも少しずつ分かってきておもしろかったです。

アリエルの開発は、プロダクト系の開発だと Java、UI 系の開発だと Javascript がメインのようです。ただプロダクトと連携するツール類は RubyPython で書かれたものもいくつかありました。なので、業務で JavaJavascript しか書けないというわけでもありません。試験的な取り組み、プロトタイプ的なツールなどは、開発者の好みの言語で実装できそうに思えました。実際、私自身も 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:アリエル・ネットワーク社の選考や採用に私は全く関係ありません

イテレーターという抽象概念にもの思い

元ネタ:

最近、Java のコードばかり書いています。

Java には、Python にはない wikipedia:抽象型 としてのインターフェースがあります。
パーフェクト Java から引用すると、インターフェースは、

オブジェクトにそのインターフェースの決めた振る舞いを期待することの表明

だと説明されています。また Java でプログラミングする上で、インターフェースを意識した抽象化が、複雑さの軽減、保守しやすいコードを書くのに重要だとあります *1

以前、EuroPython 2011 のイベントレポートを執筆した際に *2 、私はイテレーターという用語の扱い方に何となく腑に落ちていませんでした。Python では抽象クラスやインターフェースが言語機能として提供されていません。そのとき @ に「イテレーターはインターフェースだと考えたら良い」と教えてもらって、分かったような分かってないような日々を過ごしていました。

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 なオブジェクトなんだと分かるわけですね。

JavaPython で一体何が違うのか。

Javaイテレーターという抽象化された概念とその実装がインターフェースを使うことで明確に分離されています。しかし、Python の場合、分離されるかどうかは実装依存になってしまいます。MyIterable クラスを定義せずに直接、MyData クラスに __iter__() と next() メソッドを実装しても構いません。このことが私にとっては、イテレーターと呼び方を変えても、結局のところはリストやタプルのようなオブジェクトのことなんでしょ、、、といった、抽象概念としてのイテレーターと iterable なオブジェクトとの違いを混同して考えてしまう要因となっていました。

Python でプログラミングするだけなら、混同して考えてもそう害はないのかな?という気もしますが、プログラミング一般の概念として他の人と話したときにちょっと違和感を感じたものの正体がようやく分かってきた気がします。また Java を勉強していたら Python の特徴をより理解した気持ちになってちょっと嬉しかったです。

まとめると、最近、Java のコードばかり書いているんです。

パーフェクトJava (PERFECT SERIES) (PERFECT SERIES 2)

パーフェクトJava (PERFECT SERIES) (PERFECT SERIES 2)

本当の本当は誰が怖い!?

好評につき続編です。はい、嘘です。

先日、 本家Ariel Advent Calendar 2011 *1 の記事を書きましたが、今回は 元祖Ariel Advent Calendar 2011 *2 を書きます。このブログに読者という方々がいるならば、一体何をやってるんだろうか?と首を傾げたくなるでしょうが、大丈夫です。私自身、その意味も意図も、理由すら分からずに書いています。

今北産業

なぜか CTO が本家からあぶれてしまい、元祖を立ち上げて所謂、骨肉の争いに突入しました。
そして、ダークサイドに落ちたありえるたんの *3 、闇えるたん (@) も現れ、三竦みの陣容を取っています。
私は日々、3強の影に怯えながら、ひっそり開発を継続しています。

番長の暗躍

なるべく私は目立たないように開発していますが、たまに番長に見つかってしまうときがあります。

これは Trac のチケット管理のやり取りです。

とめさんをいじめて退職させる

とめさんは要領は良くないものの、がんばってテストをしていました。何とも残念なお話です。

リグレッションを起こした開発者への指導

私がある機能拡張を行った際に、そのスキーマ定義が完全ではなくて、別の箇所でリグレッションを起こしてしまいました。一旦は、私宛に担当を割り振ったものの、私が直す前に番長に修正されてしまいました。

つまらんミスをしやがって。もうお前にコードは触らせない。

と、言われているようなものです。

闇えるたんの胎動

行動がまったく読めません。

インドへ旅立った CTO を追いかけて行って闘いを繰り広げているようです。

闇えるたんよ。覚悟なさい | ありえるえりあ によると、会社ブログもクラックされたようです。

CTO との軋轢

もちろん CTO と社員との諍いも絶えません。

悪のりするときは、とことん悪のりしたら良い。

という総帥の教えに従います。

もともとアドベントカレンダーのイメージはこうでした *4

CTO がインドへ行った間隙を突いてコラージュされました。

もうやりたい放題です。

CTO は、きっと反攻の策を練っていることだと思います。

組長の真実

どうやら組長と呼ばれる人もいるそうです、まぁ、私なんですが。

一説には組長も怖いという風評があるようですが、それは明らかな間違いです。

組長は、ただ朝早いだけで人畜無害です。朝早くきて、ひっそりと開発に明け暮れてるだけのようです。

まとめ

かなり内輪感の強い内容を紹介しました。

開発というお仕事は、時にどうして良いか分からなくて不安に陥ったりします。世の中、本当に怖いことはたくさんあります。どんな状況であっても、心にゆとりをもつこと、ユーモアを受け入れること、おもしろいと思えることが大事です。

アリエルという会社の、そんな雰囲気が伝われば良いなと思います。