複数バージョンのPython向けにCI環境を構築してテストする


Python Advent Calendar25日目を担当します@giginetです。最終日なので遅れないように何とか書き上げました。

先日、Django向けに簡単なプラグインを開発しました。
そこで、実装したプラグインをPython2と3の違いを吸収して複数環境でテストするようにCI環境を構築したところ、ハマりどころが多かったため、この機会にまとめてみます。

この記事ではTravis CIを使って、複数のPythonバージョンにまたがるテストを実行して、カバレッジを計測する方法をご紹介します。

# モジュールを実装する

まず、テストしたい簡単なモジュールを実装しましょう。今回は`calculator`モジュールに`calculator.py`を置き、`Calculator`クラスを実装しました。

今回は全てのコードをPython3で実装することを想定しています。

```python:calculator/calculator.py
class Calculator(object):
    @staticmethod
    def power(number):
        return number * number
```

# テストケースを実装する

次にテストケースを実装します。ここでは単純に`Calculator#power`が動作するかをテストしています。

```python:tests/test_calculator.py
from unittest import TestCase
from calculator.calculator import Calculator


class CalculatorTestCase(TestCase):
    def setUp(self):
        self.calculator = Calculator()

    def test_power(self):
        self.assertEqual(self.calculator.power(12), 144)
```

# Test Runnerを実装する

今度は記述したテストケースを探索して、テストを実行するスクリプトを記述します。

Python2.7以上であれば、以下のように簡単にテストケースを探索してテストを実行させることができます。

```sh
$ python -m unittest discover
```

Python2.6以下もテストする場合、標準の`unittest`モジュールに、テストケースを探索する`discover`が含まれていないため、代わりに`unittest2`というパッケージを導入する必要があります。

```:requirements-test.txt
unittest2
```

```sh
$ pip install -r requirements-test.txt
```

`unittest2`を用いて、以下のようにテストケースを探索して実行するスクリプトを実装しました。

```python:runtest.py
#!/usr/bin/env python

import unittest2 as unittest

if __name__ == "__main__":
    suite = unittest.TestLoader().discover('tests', pattern = "test_*.py")
    unittest.TextTestRunner().run(suite)
```

これを実行すると下記のようにテストが走ります。

```sh
$ chmod +x runtest.py
$ ./runtest.py
.
----------------------------------------------------------------------
Ran 1 tests in 0.001s

OK
```

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

次はいよいよ、このテストケースを複数の環境で走らせてみましょう。

一つのソースを複数バージョンのPythonやDjangoで簡単にテストする方法として、 **tox**を利用することがよく知られています。

[Welcome to the tox automation project — tox 2.3.1 documentation](https://tox.readthedocs.org/en/latest/)

toxは様々な条件を変えて、テスト環境を構築して、複数の環境でまとめてテストを実行することができるツールです。

`tox.ini`という設定ファイルを定義することでテスト環境を構築することができるので、今回は以下のように定義します。

```tox.ini
[tox]
skipsdist = True
envlist =
    py26,
    py27,
    py33,
    py34,
    py35

[testenv]
deps=
    -rrequirements-test.txt
commands=
  ./runtest.py
```

詳しくはドキュメントを参照してください。

[tox configuration specification — tox 2.3.1 documentation](https://tox.readthedocs.org/en/latest/config.html)

最後に、`tox`を実行すると全てのテスト環境を同時に走らせることができます。

今回は動作確認のためにPython3.5でのみテストを実行してみます。

```sh
$ tox -r -e 'py35'
```

これで複数のPythonバージョンに対応したテスト環境の構築ができました。


# tox-travisでTravis CI上でtoxを走らせる

今度はこれを[Travis CI](https://travis-ci.org/)上で走らせてみましょう。

今回は`tox-travis`というモジュールを使います。

[ryanhiebert/tox-travis](https://github.com/ryanhiebert/tox-travis)

これを導入すると、Travis CI上でtoxを動かすための設定を省略することができます。

例えば下記のように`.travis.yml`を用意するだけで複数の環境でテストが実行できます。

```yaml:.travis.yml
sudo: false
language: python
python:
  - 2.6
  - 2.7
  - 3.3
  - 3.4
  - 3.5

install:
  - pip install tox tox-travis
  - pip install coverage coveralls

script:
  - tox -r
```

これをTravis CI上で実行すると、以下のように複数のコンテナが立ち上がり、簡単にテストすることができます。便利ですね。

![Screen Shot 2015-12-25 at 01.30.33.png](https://qiita-image-store.s3.amazonaws.com/0/10009/0c737019-bc5b-3a7b-7e89-ae43ee1f86b4.png "Screen Shot 2015-12-25 at 01.30.33.png")


# `__future__`モジュールを使ってPython2と3の互換性を保つ

Python3向けに書いたコードをPython2で動かすためには、`__future__`モジュールなどを使い、後方互換性を保った書き方をしなくてはなりません。

この記事では詳しく述べないため、詳しくはドキュメントを参照してください。

[Easy, clean, reliable Python 2/3 compatibility — Python-Future documentation](http://python-future.org/index.html)

# Python2でmockテスト

モックやスタブを使ったテストを行いたい場合、Python3には標準ライブラリにmockモジュールが含まれています。

[26.5. unittest.mock — mock object library — Python 3.5.1 documentation](https://docs.python.org/3/library/unittest.mock.html)

一方でPython2以前の環境においてはmockを標準で使うことができません。

Python3のmockモジュールを2以前にバックポートしたパッケージが存在するので、それを導入しましょう。

```:requirements-test.txt
mock
```

まず`unittest.mock`がimportできるかを試し、標準ライブラリに含まれていなかった場合、`mock`ライブラリからimportするようにしましょう。

```python
from unittest import TestCase

try:
    from unittest.mock import MagicMock
except ImportError:
    from mock import MagicMock


class CalcTestCase(TestCase):
    def test_mock(self):
        mock_object = MagicMock()
        mock_object.__str__.return_value = 'hello'
        self.assertEqual(str(mock_object), 'hello')
```

このようにすると、Python2でも3同様にmockテストを行うことができます。

mockの詳しい使い方については以下を参照すると良いでしょう。

[Python - もっくもっく～unittest.mock, mock.mock revisit～ - Qiita](http://qiita.com/sakamotomsh/items/ab6c15b971587905ef43)


# カバレッジを計測する

全ての環境でテストが通るようになったら、最後にカバレッジを計測して、[coverails](https://coveralls.io/)で管理しましょう。

まず、`tox.ini`で以下のように`coverage`を実行してカバレッジを計測します。

```:tox.ini
deps=
    -rrequirements-test.txt
    coverage
commands=
  {envbindir}/coverage run --append --source=calculator runtest.py
```

このとき、`.coveragerc`というファイルを定義しておくと、カバレッジ計測のルールを設定することができます。以下では、カバレッジ計測を行わないファイルや行を定義しています。

詳しくは以下をご覧ください。

[Configuration files — Coverage.py 4.0.3 documentation](http://coverage.readthedocs.org/en/latest/config.html)

```:.coveragerc
[report]
include =
    calculator/*.py
omit =
    calculator/__init__.py
exclude_lines =
    pragma: no cover
    if __name__ == .__main__.:
```

最後に`.travis.yml`に以下を追記し、テスト終了後に`coveralls`にログを送るようにします。

```yaml:.travis.yml
install:
  - pip install tox tox-travis
  - pip install coverage coveralls

after_success:
  - coverage report
  - coveralls
```

これでCoverallsでカバレッジを記録できるようになりました。

![Screen Shot 2015-12-25 at 01.24.24.png](https://qiita-image-store.s3.amazonaws.com/0/10009/081a9653-e76c-05b2-95e4-6d550a646db2.png "Screen Shot 2015-12-25 at 01.24.24.png")


# まとめ

この記事では、Pythonのライブラリを複数バージョンでテストする方法について簡単にまとめました。

今回紹介したTest Suiteを以下のリポジトリに簡単にまとめてあります。

https://github.com/giginet/python-ci-suite

また、`tox`を上手く使えば、あるライブラリについて複数のバージョンでテストすることもできます。

以下のDjangoプラグインでは、複数バージョンのPythonとDjangoの組み合わせでテストしています。
ご興味のある方はぜひ参考にしてみてください。

[giginet/django-debug-toolbar-vcs-info](https://github.com/giginet/django-debug-toolbar-vcs-info)