CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: tox を使って複数のバージョンでテストを実行する

先日の記事で Python 2.x/3.x の互換性に関するツールをいくつか紹介したけど、詳しい使い方までは書くことができなかった。 今回は、その中のひとつ tox の使い方について紹介してみる。

なんで tox が必要なのか

プロジェクトを複数のバージョンの Python に対応させるには自動化されたテストが必要になる。 本当にそのバージョンでソースコードが動作するかは、実際に動かしてみないと分からないため。

ただ、テストがあったとしても複数のバージョンでそれを実行するのを手動でやっていては手間もかかるしミスも出る。 tox は複数のバージョンで一度にテストを実行してくれるため、その手間とミスを大幅に減らすことができる。

記事の全体の流れ

今回は tox を実際にサンプルのプロジェクトを用意して使ってみることにする。 流れは次の通り。

  • pyenv で複数のバージョンの Python をインストールする
  • テスト対象と nose で書かれたテストを用意する
  • 上記を setuptools でインストールできるパッケージにする
  • パッケージを tox を使ってテストする

尚、環境には CentOS7 を使った。

$ cat /etc/redhat-release
CentOS Linux release 7.1.1503 (Core)
$ uname -r
3.10.0-229.11.1.el7.x86_64

pip をインストールする

まずは、あらかじめ Python のパッケージマネージャ pip をインストールしておく。 同時にパッケージング用のライブラリである setuptools もインストールされる。

$ sudo yum -y install wget
$ wget -O - https://bootstrap.pypa.io/get-pip.py | sudo python

pyenv をインストールする

次に、複数のバージョンの Python を管理するのに便利な pyenv をインストールするために Git のリポジトリをチェックアウトする。

$ sudo yum -y install git
$ git clone https://github.com/yyuu/pyenv.git ~/.pyenv

pyenv にパスを通すためにシェルの起動スクリプトに設定を追加する。 bash 以外のシェルを使う場合には、起動スクリプトの名前をそのシェルのものに変える。

$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile

シェルに設定を反映すると pyenv コマンドが使えるようになる。

$ source ~/.bash_profile
$ which pyenv
~/.pyenv/bin/pyenv

そして Python のビルドに必要な依存パッケージをインストールしておく。

$ sudo yum -y install zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel patch

pyenv で Python をインストールする

pyenv install サブコマンドにお目当てのバージョンを指定することで Python をインストールできる。

$ pyenv install 3.4.3

とりあえず今のライブラリなら 3.4 3.3 2.7 2.6 あたりをサポート対象にすれば良いんじゃないかなーと思うのでひと通りインストールしておく。

$ for i in 3.3.6 2.7.10 2.6.9; do pyenv install $i; done;

使用するパッチバージョンを指定するため pyenv の設定ファイルに書き出しておく。

$ cat << EOF > ~/.pyenv/version
system
3.4.3
3.3.6
2.7.10
2.6.9
EOF

これでマイナーバージョンまでの指定で特定のバージョンの Python が使えるようになった。

$ python3.4 --version
Python 3.4.3
$ for i in 3.3 2.7 2.6; do python${i} --version;done;
Python 3.3.6
Python 2.7.5
Python 2.6.9

テスト対象のパッケージを作る

ここからはテスト対象のパッケージを作っていく。 といってもサンプルなのでシンプルに。 まずは Python のパッケージとして認識させるために sample という名前でディレクトリを作った上で __init__.py というファイルを作っておく。

$ mkdir sample
$ touch sample/__init__.py

実際に動作するコードとしては calc というモジュールの中に add() という名前でふたつの引数を足し算するだけの関数を作っておく。

$ cat << EOF > sample/calc.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def add(a, b):
    return a + b
EOF

テストを作る

次に、先ほど作った calc モジュールに対するテストを作る。 sample パッケージの中に、更に tests パッケージを作った上で test_calc というモジュールを用意する。 今回は nose というサードパーティ製のテスティングフレームワークを使う想定でテストを書いた。

$ mkdir sample/tests
$ touch sample/tests/__init__.py
$ cat << EOF > sample/tests/test_calc.py
# -*- coding: utf-8 -*-

import nose
from nose.tools import assert_equal

from sample.calc import add


class Test_sample(object):

    def test(self):
        assert_equal(add(1, 1), 2)

if __name__ == '__main__':
    nose.main(argv=['nosetests', '-s', '-v'], defaultTest=__file__)
EOF

ここまででディレクトリの構成は次のようになっている。

$ find sample/
sample/
sample/__init__.py
sample/calc.py
sample/tests
sample/tests/__init__.py
sample/tests/test_calc.py

nose でテストを実行する

上記で作ったテストを nose で実行する。

まずは pip コマンドで nose をインストールしよう。

$ sudo pip install nose

nose をインストールすると nosetests コマンドが使えるようになる。 これは指定したディレクトリ以下にあるテストを自動的に探してきて実行してくれる便利なやつ。 テストの探し方は test なんちゃらって名前になってるファイル名やクラス名、メソッド名を手がかりにする。

$ nosetests -v sample/
test_calc.Test_sample.test ... ok

----------------------------------------------------------------------
Ran 1 test in 0.002s

OK

まずは、システムにインストールされているデフォルトの Python でちゃんとテストがパスすることが確認できた。

パッケージをインストールできるようにする

tox でテストするにはパッケージがインストールできる形式になっている必要がある。 なぜかというと、tox は Python 仮想環境を作った上でそこにテスト対象のパッケージを展開して実行するという動作原理になっているため。

ということでインストールに必要なセットアップスクリプトをささっと書く。 最近の Python パッケージでは、依存しているサードパーティ製のライブラリをテキストファイルに記述しておくのがデファクトスタンダードになりつつあるので、それを読み込むように作った。

$ cat << EOF > setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup
from setuptools import find_packages


def _requirements():
    return [
        name.rstrip()
        for name in open('requirements.txt').readlines()
    ]


def _test_requirements():
    return [
        name.rstrip()
        for name in open('test-requirements.txt').readlines()
    ]


def main():
    setup(
        name='sample',
        version='0.0.1',
        description='Sample package',
        author='momijiame',
        author_email='momijiame@example.jp',
        packages=find_packages(),
        install_requires=_requirements(),
        test_require=_test_requirements(),
        test_suite = 'nose.collector',
        zip_safe=False,
        include_package_data=True,
    )


if __name__ == '__main__':
    main()
EOF

上記で読み込んでいる依存ライブラリを記述したテキストファイルを用意する。 インストールに必要なものはひとつもないけど、テストに必要なものについては先ほどの nose がある。

$ touch requirements.txt
$ cat << EOF > test-requirements.txt
nose
EOF

Python のソースコード以外をパッケージに含める場合は MANIFEST.in というファイルも必要になるので書いておく。

$ cat << EOF > MANIFEST.in
include requirements.txt
include test-requirements.txt
EOF

virtualenv で動作を確認する

作成したパッケージを実際にインストールしてみよう。 とはいえシステムに入れてしまうのはイマイチなので virtualenv を使う。 virtualenv はシステムから独立した Python 実行環境 (Python 仮想環境と呼ばれる) を作ることができるサードパーティ製のライブラリで tox もこれを使っている。

$ sudo pip install virtualenv

Python 仮想環境を作ってアクティベートする。

$ virtualenv /tmp/sample-env
New python executable in /tmp/sample-env/bin/python
Installing setuptools, pip, wheel...done.
$ source /tmp/sample-env/bin/activate

すると、システムにインストールされていたパッケージは見えなくなり、独立した実行環境の中にいることがわかる。

(sample-env)$ pip list
pip (7.1.2)
setuptools (18.2)
wheel (0.24.0)

作成したパッケージをインストールする。

(sample-env)$ python setup.py install
(sample-env)$ pip list
pip (7.1.2)
sample (0.0.1)
setuptools (18.2)
wheel (0.24.0)

そして add() 関数をワンライナーで使ってみる。

(sample-env)$ python -c "from sample.calc import add; print(add(1, 1))"
2

ばっちり。

ひと通り確認できたら、ひとまず仮想環境から抜ける。

$ deactivate

tox を使って各バージョンでテストを実行する

さて、やっとここからが本題。 tox をインストールする。

$ sudo pip install tox

tox の設定ファイルを用意する。 tox セクションの envlist にはテスト対象のバージョンを羅列する。 testenv セクションの deps には依存ライブラリのインストール方法を指定する。 同じく commands には、準備が整った上で実行したいコマンドを指定する。 ここでは nosetests でテストを実行しているが、例えば pep8 や pylint などを実行してももちろん構わない。

$ cat << EOF > tox.ini
[tox]
envlist = py26,py27,py33,py34

[testenv]
deps =
    -U
    -r{toxinidir}/requirements.txt
    -r{toxinidir}/test-requirements.txt
commands =
    nosetests -v
EOF

設定ファイルが用意できたら tox コマンドを実行する。 すると各バージョンの Python 仮想環境上にパッケージが展開された上で指定したコマンドが実行される。

$ tox
GLOB sdist-make: /home/vagrant/setup.py
py26 inst-nodeps: /home/vagrant/.tox/dist/sample-0.0.1.zip
py26 installed: argparse==1.3.0,nose==1.3.7,pep8==1.6.2,sample==0.0.1,wheel==0.24.0
py26 runtests: PYTHONHASHSEED='3203418270'
py26 runtests: commands[0] | nosetests -v
test_calc.Test_sample.test ... ok

----------------------------------------------------------------------
Ran 1 test in 0.003s

OK
py27 create: /home/vagrant/.tox/py27
py27 installdeps: -U, -r/home/vagrant/requirements.txt, -r/home/vagrant/test-requirements.txt
py27 inst: /home/vagrant/.tox/dist/sample-0.0.1.zip
py27 installed: nose==1.3.7,sample==0.0.1,wheel==0.24.0
py27 runtests: PYTHONHASHSEED='3203418270'
py27 runtests: commands[0] | nosetests -v
test_calc.Test_sample.test ... ok

----------------------------------------------------------------------
Ran 1 test in 0.003s

OK
py33 create: /home/vagrant/.tox/py33
py33 installdeps: -U, -r/home/vagrant/requirements.txt, -r/home/vagrant/test-requirements.txt
py33 inst: /home/vagrant/.tox/dist/sample-0.0.1.zip
py33 installed: nose==1.3.7,sample==0.0.1,wheel==0.24.0
py33 runtests: PYTHONHASHSEED='3203418270'
py33 runtests: commands[0] | nosetests -v
test_calc.Test_sample.test ... ok

----------------------------------------------------------------------
Ran 1 test in 0.006s

OK
py34 create: /home/vagrant/.tox/py34
py34 installdeps: -U, -r/home/vagrant/requirements.txt, -r/home/vagrant/test-requirements.txt
py34 inst: /home/vagrant/.tox/dist/sample-0.0.1.zip
py34 installed: nose==1.3.7,sample==0.0.1,wheel==0.24.0
py34 runtests: PYTHONHASHSEED='3203418270'
py34 runtests: commands[0] | nosetests -v
test_calc.Test_sample.test ... ok

----------------------------------------------------------------------
Ran 1 test in 0.004s

OK
_____________________________________________________ summary _____________________________________________________
  py26: commands succeeded
  py27: commands succeeded
  py33: commands succeeded
  py34: commands succeeded
  congratulations :)

ばっちり。

最終的なディレクトリ構成はこんなかんじ。 (ビルドなどの際に生成されるディレクトリやファイルは除外している)

$ find .
.
./MANIFEST.in
./requirements.txt
./sample
./sample/__init__.py
./sample/calc.py
./sample/tests
./sample/tests/__init__.py
./sample/tests/test_calc.py
./setup.py
./test-requirements.txt
./tox.ini

いじょう。

まとめ

今回は tox を使ってテストを実行するまでの手順を、実際にサンプルとなるパッケージを動作確認を交えつつ作った上で実施してみた。