naritoブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

Djangoアプリを配布する〜⑥おまけ

プログラミング関連 Django Python tox coveralls 約131日前
2017年8月2日21:54
前記事
Djangoアプリを配布する〜⑤PyPI(testpypi)に登録し、pipでインストールする
https://torina.top/detail/377/

今回は、今まで作ったものをもう少し洗練していきます。

まずですが、ディレクトリの構成を変えていきます。
myproject、myappと同階層にtestsというディレクトリを作成し、4つのファイルを作成してください。
myapp/
    ...
    ...
myproject/
    ...
    ...
tests/
    __init__.py
    test_views.py
    settings.py
    urls.py


__init__.py


__init__.pyは空欄で構いません。

test_views.py


test_views.pyは、myappのtests.pyと中身は同じで大丈夫です。
myappのtests.pyは、もう消して大丈夫です。


settings.py


テスト用の設定ファイルとなります。
プロジェクトのsettings.pyを流用しても大丈夫っちゃ大丈夫ですが、基本的には最小限の設定だけ書いたテスト用settings.pyを用意するのが一般的のようです。
今回のテストならば以下の設定だけで動きますが、エラーが出たりしたら都度足していきましょう。
SECRET_KEY = 'django-torina-tutorial1'

INSTALLED_APPS = [
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'myapp',
]


ROOT_URLCONF = 'tests.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': ':memory:',
    }
}


tests/urls.pyを利用するように指定しています。
ROOT_URLCONF = 'tests.urls'


'NAME': ':memory:',とすることで、メモリ内にストレージを作ってくれます。テストならば、基本的にこれでも大丈夫です。
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': ':memory:',
    }
}



urls.py


テストで使うURLとviewの紐付けを書きます。
from django.conf.urls import url, include

urlpatterns = [
    url(r'^', include('myapp.urls')),
]



python manage.py test
と行うと、ちゃんとtests以下のテストも実行されるはずです。
動くかを確認しておきましょう。


setup.py


更に、python setup.py test でテストを行えるようにしましょう。
Djangoに限らず、どのような種類のプログラムであってもsetup.py test でテストが行えるようにするのは良いプラクティクスです。
setup.pyを以下のようにしましょう。
import os
import sys
from setuptools import find_packages, setup
from setuptools.command.test import test as TestCommand

with open(os.path.join(os.path.dirname(__file__), 'README.rst'), 'rb') as readme:
    README = readme.read()

# allow setup.py to be run from any path
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))


class DjangoTest(TestCommand):

    def run_tests(self):
        #import here, cause outside the eggs aren't loaded
        import django
        from django.conf import settings
        from django.test.utils import get_runner
        os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings'
        django.setup()
        TestRunner = get_runner(settings)
        test_runner = TestRunner()
        failures = test_runner.run_tests(['tests'])
        sys.exit(bool(failures))


setup(
    name='django-torina-tutorial1',
    version='0.1',
    packages=find_packages(exclude=('myproject', 'tests')),
    include_package_data=True,
    license='MIT License',
    description='Django Simple App',
    long_description=README.decode(),
    url='https://github.com/naritotakizawa/django-torina-tutorial1',
    author='Narito Takizawa',
    author_email='toritoritorina@gmail.com',
    classifiers=[
        'Environment :: Web Environment',
        'Framework :: Django',
        'Framework :: Django :: 1.11',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: MIT License',
        'Operating System :: OS Independent',
        'Programming Language :: Python',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.6',
    ],
    cmdclass = {'test': DjangoTest},
)


pipでインストールする際に、testsは含まないようにするのを忘れないようにします。
packages=find_packages(exclude=('myproject', 'tests')),


テストの設定部分は以下です。②で紹介したpytestの例と流れは同じです。
from setuptools.command.test import test as TestCommand
...
...
class DjangoTest(TestCommand):

    def run_tests(self):
        #import here, cause outside the eggs aren't loaded
        import django
        from django.conf import settings
        from django.test.utils import get_runner
        os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings'
        django.setup()
        TestRunner = get_runner(settings)
        test_runner = TestRunner()
        failures = test_runner.run_tests(['tests'])
        sys.exit(bool(failures))


setup(
    ...
    ...
    cmdclass = {'test': DjangoTest},
)


python setup.py test で無事にテストが行われれば、OKです。


ついでにですが、myappを仮想環境内にpipでインストールしておきましょう。
Djangoアプリの場合はしなくてもあまり不都合はないのですが、作成するプログラムによっては必須となりますので、忘れずにやっておきます。
ただ、普通にpipでインストールするとmyappを変更するたびにインストールしなおしになりますので、以下のようなオプションをつけてインストールします。
pip install -e .


-eオプションをつけることで、この問題は解決します。
また、今回はsetup.pyと同階層でコマンドを実行したので、「.」という指定にしています。


今まではmyprojectディレクトリやmanage.pyはテストのためにあえて残しておいたのですが、tests内にテスト環境も設定したのでもう不要です。
.gitignoreを編集し、Githubにもあげないようにしておきましょう。

.gitignore


.toxや.coverrageは、この後追加しますので、先に含めておきました。
*.pyc
*.pyo
.eggs
*.egg-info
.cache
.coverage
.tox
__pycache__

db.sqlite3
manage.py
/myproject/

dist/
build/
django_torina_tutorial.egg-info/


既にGithubにはmyprojectディレクトリ等を含めてしまっています。手作業で除外するのは面倒なので、以下のコマンドをうちましょう。
gitignoreにある設定を読み込み、登録されているものがあれば除外してくれます。
git rm --cached `git ls-files --full-name -i --exclude-from=.gitignore`



Githubの優れたPythonライブラリを見ると、「tox」がとてもよく利用されています。多くのリポジトリで、tox.iniというtoxの設定ファイルを見かけることでしょう。
ドキュメントが以下にあるので、興味がある人は以下です。
http://tox.readthedocs.io/en/latest/

toxは複数のvirtualenv環境にテスト環境を構築してくれます。
複数のPythonのバージョンのテストはTravis CIでもできましたが、toxを使うとローカル環境でもそれができます。

pip install toxでインストールし、tox.iniを作り、toxコマンドで実行されます。
早速、今回のDjangoアプリでもtoxを使ってみましょう。
pip install tox


tox.ini


最もシンプルな例は、以下のようになります。
[tox]
envlist = {py34,py35,py36}-{dj110,dj111,djmaster}


[testenv]
deps =
    dj110: django==1.10
    dj111: django==1.11
    djmaster: https://github.com/django/django/archive/master.tar.gz

commands = 
    python -Wall setup.py test



envlistはテスト環境の一覧の設定です。
これは実行した結果を見るとしっくりきます。
envlist = {py34,py35,py36}-{dj110,dj111,djmaster}


以下が実行結果です。
〜実行中のログが色々と流れる〜
  py34-dj110: commands succeeded
  py34-dj111: commands succeeded
  py34-djmaster: commands succeeded
  py35-dj110: commands succeeded
  py35-dj111: commands succeeded
  py35-djmaster: commands succeeded
  py36-dj110: commands succeeded
  py36-dj111: commands succeeded
  py36-djmaster: commands succeeded
  congratulations :)



py34やpy35といった名前は特別な意味を持ち、これはpython3.4や3.5で仮想環境を作成してくれます。
python3.4や3.5が入っていない方は、インストールしておきましょう。

[testenv]セクションはテスト環境の設定を書いていきます。
depsはインストールするライブラリ等を定義する場所で、以下はdj110環境でDjango1.10、dj111環境でDjango1.11、djmaster環境でDjangoのGithubマスターブランチからインストールしています。
deps =
    dj110: django==1.10
    dj111: django==1.11
    djmaster: https://github.com/django/django/archive/master.tar.gz


commandsには、どのようなコマンドを打つかを記入します。見たまんまですね。
commands =  python -Wall setup.py test


これはこれでシンプルで良いのですが、もう少しテストを便利にしたいところです。
まずはテストのカバレッジを取得するようにしましょう。
depsにcoverageを追加し、commandsをcoverage用に変更するだけです。
--source myappは、カバレッジを取得するソースをmyappディレクトリ以下だけにしています。複数必要な場合は、myapp,other のように指定します。
[tox]
envlist = {py34,py35,py36}-{dj110,dj111,djmaster}

[testenv]
deps =
    coverage
    dj110: django==1.10
    dj111: django==1.11
    djmaster: https://github.com/django/django/archive/master.tar.gz

commands = 
    coverage run --source myapp setup.py test
    coverage report -m



以下のような感じで表示されます。わかりやすいですね。
Missingの列は、そのモジュールのテストが通っていない行です。
つまり、admin.pyの1-4行目、apps.pyの1-5行目はテストがされていないということです。


もうひと頑張りしましょう。flake8で、書いたコードのチェックもするようにしましょう。
[tox]
envlist = flake8,{py34,py35,py36}-{dj110,dj111,djmaster}


[testenv]
deps =
    coverage
    dj110: django==1.10
    dj111: django==1.11
    djmaster: https://github.com/django/django/archive/master.tar.gz

commands = 
    coverage run --source myapp setup.py test
    coverage report -m


[testenv:flake8]
basepython = python3.6
deps = flake8
commands = flake8 myapp

[flake8]
exclude = myapp/migrations/*


まずここにflake8という名前を追加しました。
envlist = flake8,{py34,py35,py36}-{dj110,dj111,djmaster}


[testenv:flake8]と書くと、flake8環境の際にこの設定が適用されます。専用の設定がある場合は、[testenv]は適用されなくなります。
実行するpythonを3.6にしておき、depsでflake8をインストール、実行コマンドはflake8 myapp としています。testsディレクトリ以下も含めたい場合はflake8 myapp tests のようにします。
また、migrations以下をチェックしないよう、excludeで指定しています。
[testenv:flake8]
basepython = python3.6
deps = flake8
commands = flake8 myapp

[flake8]
exclude = myapp/migrations/*


今回はpipでインストールできるプログラムを作りましたが、pipでインストールしないタイプのプログラムを公開することもよくあります。
例えばDjangoならば、Djangoプロジェクト自体をGithubに公開することもあるでしょう。このような場合、setup.pyは作成しないはずです。
その場合は、tox.iniに以下のようにskipdist = true を追加します。
[tox]
envlist =flake8, ...
skipsdist = true



toxはこの辺で大丈夫でしょう。
次は.travis.ymlです。

.travis.yml


こちらもsetup.py test でテストするように修正しておきましょう。
travis.ymlからtoxを呼ぶ方もよくいますが、むつかしくなるので私は別々にしています。
language: python
 
python:
    - "3.4"
    - "3.5"
    - "3.6"
 
env:
  - DJANGO=django==1.10
  - DJANGO=django==1.11
  - DJANGO=https://github.com/django/django/archive/master.tar.gz
 
install:
    - pip install DJANGO
 
script:
    - python -Wall setup.py test



travis ciが走るたびに、カバレッジを取得し表示できるようにすると更に良さそうです。
テストカバレッジを取得するCIサービスですが、有名なのは「Codecov」と「Coveralls」です。今回はCoverallsでやってみます。

さっそく以下にアクセスしましょう。
https://coveralls.io/


サインインから、Githubでのサインインを選択しましょう。
Travis CIと流れは同じで、画面左側メニューの「ADD REPOS」から連携したいGithubリポジトリを選択します。

こんな画面になるので、追加しましょう。ONにしたらOKです。


travis.ymlを修正しましょう。
language: python

before_script:
  - pip install coveralls

python:
    - "3.4"
    - "3.5"
    - "3.6"
 
env:
  - DJANGO=django==1.10
  - DJANGO=django==1.11
  - DJANGO=https://github.com/django/django/archive/master.tar.gz
 
install:
    - pip install DJANGO
 
script:
    - coverage run --source myapp setup.py test

after_success:
    - coveralls


先にcoverallsをインストールしておく、という記述です。
before_script:
  - pip install coveralls


カバレッジを取得するように、実行コマンドを少し変更します。今回はそのまま表示してもアレなので、coverage report -m等はいりません。
script:
    - coverage run --source myapp setup.py test


テストが失敗しなければ、ここです。これで取得したカバレッジを送信してくれます。
送信したカバレッジの詳細は、coverallsのサイトで見ることができます。
after_success:
    - coveralls


Travis CIと同様に、Coveralls等でもバッジが提供されています。これはGithubで「coverage:100%」 のように表示されるバッジです。(.rstファイル用です)
リードミーに、以下を足しておきましょう。django-torina-tutorial1部分が、Githubのリポジトリの名前になります。
.. image:: https://coveralls.io/repos/github/naritotakizawa/django-torina-tutorial1/badge.svg
    :target: https://coveralls.io/github/naritotakizawa/django-torina-tutorial1


これで割とイケてる感じになりました。次回はDjangoではなく、もう少し一般的なプログラムで、pytestを使った例の紹介です。
通常のPythonライブラリ・フレームワークを配布する
https://torina.top/detail/388/