CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: テストフレームワーク nose のプラグインを書いてみる

nose は Python のテストフレームワークのひとつ。 特徴のひとつとしてプラグイン機構があるためテストランナーの挙動をカスタマイズできる点が挙げられる。

プラグインの書き方は次のページに詳しく書かれている。 Writing Plugins — nose 1.3.7 documentation

今回は上記のページを元に簡単なプラグインをいくつか書いてみることにする。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.3
BuildVersion:   15D21
$ python --version
Python 3.5.1

プラグインを書いてみる

プラグインは nose_helloworld という名前のパッケージとして用意する。 まずはプロジェクト用のディレクトリを用意しておこう。

$ mkdir nose_helloworld
$ cd nose_helloworld

ディレクトリの中に移動したら、さらに同じ名前でディレクトリを作る。 ただし、今度のディレクトリは Python のパッケージにするためのものだ。 Python のパッケージは __init__.py というファイルの入ったディレクトリとして表現される。

$ mkdir nose_helloworld

ちなみに Python のモジュールとパッケージまわりの話はこちらの記事に書いたので併せて参照してもらいたい。

blog.amedama.jp

それでは、先ほどの説明通りディレクトリの中に __init__.py を作ろう。 これで nose_helloworld ディレクトリは Python の nose_helloworld パッケージとして認識できるようになる。 今回は、この init.py の中に nose のプラグインを書くことにする。

nose のプラグインは nose.plugins.Plugin のサブクラスとして定義する。 書き方にはいくつかの流儀があるけど、基本的にはスーパークラスにあるメソッドのオーバーライドするだけで良い。 他にはクラスの docstring がコマンドラインのヘルプメッセージになるので書いておいた方が良い。 あとはプラグイン名を name メンバに入れておくのも忘れずに。 ちなみに、オーバーライドしたメソッドの finalize() はテストが完了したときに呼び出されるコールバックだ。 最初のプラグインでは、ここでログを出すことにする。

$ cat << 'EOF' > nose_helloworld/__init__.py
# -*- coding: utf-8 -*-

import logging

from nose.plugins import Plugin

LOG = logging.getLogger('nose.plugins.helloworld')


class HelloWorldPlugin(Plugin):
    """My first plugin"""
    name = 'helloworld'

    def finalize(self, result, *args, **kwargs):
        LOG.info('HelloWorld Plugin: finalize')

EOF

プラグインで使えるコールバックは、次の IPluginInterface クラスで確認できる。 この中で <メソッド>._new = True となっているのが使って良いものみたい。 github.com

さて、これでパッケージの中にプラグインが用意できた。 次はパッケージをインストールできるようにする。 なぜなら nose のプラグイン機構は setuptools というパッケージング用の API を使っているためだ。 具体的には pkg_resources というものだけど、これはパッケージをインストールするためのセットアップスクリプトの中に指定する。

次のセットアップスクリプトでは setup() 関数の中の entry_points 引数に 'nose.plugins.0.10' という項目を用意している。 nose のプラグインになるクラスはここで指定することになる。

$ cat << 'EOF' > setup.py
# -*- coding: utf-8 -*-

from setuptools import setup

def main():
    setup(
        name='nose_helloworld',
        version='0.0.1',
        zip_safe=False,
        packages=['nose_helloworld'],
        install_requires=[
            'nose==1.3.7',
        ],
        entry_points = {
            'nose.plugins.0.10': [
                'helloworld = nose_helloworld:HelloWorldPlugin'
            ],
        },
    )


if __name__ == '__main__':
    main()

EOF

パッケージをインストールする

これで nose_helloworld パッケージをインストールする用意ができた。 セットアップスクリプトに install サブコマンドを渡して実行しよう。

$ python setup.py install

これで nose_helloworld パッケージがインストールされた。

$ pip freeze | grep helloworld
nose-helloworld==0.0.1

プラグインを使う

パッケージをインストールすると nose のテストランナーである nosetests コマンドに --with-helloworld というオプションが追加される。 これは先ほどインストールしたパッケージの中に HelloWorldPlugin にある name メンバと docstring から生成されている。 このオプションを有効にすることで作成した HelloWorldPlugin が有効になるという寸法だ。

$ nosetests --help | grep -A 1 helloworld
 --with-helloworld     Enable plugin HelloWorldPlugin: My first plugin for
                       nose [NOSE_WITH_HELLOWORLD]

それでは HelloWorldPlugin を有効にして nosetests を実行してみよう。

$ nosetests -vv --with-helloworld
nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$']

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
nose.plugins.helloworld: INFO: HelloWorld Plugin: finalize

nose.plugins.helloworld という名前でログが出力されたことがわかる。 ちゃんとテストが完了したときにプラグインのコードが実行された!

ちなみにヘルプメッセージにもあったようにプラグインは環境変数を使って有効にすることもできる。 プラグインを表すクラスの name メンバに 'helloworld' が格納されている場合は NOSE_WITH_HELLOWORLD という環境変数名になる。

$ export NOSE_WITH_HELLOWORLD=1
$ nosetests -vv
nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$']

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
nose.plugins.helloworld: INFO: HelloWorld Plugin: finalize

プラグインにオプションを追加する

先ほどの例ではこれ以上ないほどシンプルなプラグインを扱った。 次はプラグインに動作を変更するためのオプションを追加してみよう。

先ほどのプラグインを上書きする形で変更していく。 今度は options() というメソッドと configure() というメソッドをプラグインに実装しよう。 options() メソッドは argparse の API を使ってプラグインが有効なときに使えるコマンドラインオプションが追加できる。 同様に configure() メソッドでは、それらのオプションの値を利用してプラグインを設定できる。 次のプラグインでは --message というオプションを追加した。 先ほどは固定だった finalize() メソッドで出力されるログのメッセージを、このオプションで指定されたものに差し替えてみよう。

$ cat << 'EOF' > nose_helloworld/__init__.py
# -*- coding: utf-8 -*-

import logging

from nose.plugins import Plugin

LOG = logging.getLogger('nose.plugins.helloworld')


class HelloWorldPlugin(Plugin):
    """My first plugin"""
    name = 'helloworld'

    def options(self, parser, env, *args, **kwargs):
        super(HelloWorldPlugin, self).options(parser, env, *args, **kwargs)
        parser.add_option(
            '--message',
            default='Hello, World!',
            help='Greeting message',
        )

    def configure(self, options, conf, *args, **kwargs):
        super(HelloWorldPlugin, self).configure(options, conf, *args, **kwargs)
        self.message = options.message

    def finalize(self, result, *args, **kwargs):
        LOG.info('HelloWorld Plugin: {msg}'.format(msg=self.message))

EOF

プラグインを上書きしたらパッケージをインストールし直そう。

$ python setup.py install

もう一度 nosetests コマンドをプラグインを有効にして実行してみる。

$ nosetests -vv --with-helloworld
nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$']

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
nose.plugins.helloworld: INFO: HelloWorld Plugin: Hello, World!

先ほどとは finalize() メソッドで出力されるログのメッセージが変更されていることがわかる。

次にコマンドラインオプションとして --message を追加しよう。 ここに出力してほしいログメッセージを指定する。

$ nosetests -vv --with-helloworld --message 'Hello, Plugin!'
nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$']

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
nose.plugins.helloworld: INFO: HelloWorld Plugin: Hello, Plugin!

--message オプションで指定したメッセージが出力されていることがわかる。

別のコールバックも実装してみる

次は nose のテストランナーがそれぞれのテストを実行する直前に呼び出されるコールバックをプラグインに実装してみよう。 これには beforeTest() というメソッドをプラグインに実装する。

$ cat << 'EOF' > nose_helloworld/__init__.py
# -*- coding: utf-8 -*-

import logging

from nose.plugins import Plugin

LOG = logging.getLogger('nose.plugins.helloworld')


class HelloWorldPlugin(Plugin):
    """My first plugin"""
    name = 'helloworld'

    def beforeTest(self, test, *args, **kwargs):
        LOG.info('HelloWorld Plugin: {test}'.format(test=test))

EOF

用意できたらパッケージをインストールし直そう。

$ python setup.py install

動作確認のためにシンプルなテストを用意する。 これは、実際には何もテストしていない。

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

from nose.tools import ok_


class Test(object):

    def test(self):
        ok_(True)

EOF

準備ができたら nosetests コマンドを実行する。

$ nosetests -vv --with-helloworld
nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$']
nose.plugins.helloworld: INFO: HelloWorld Plugin: test_helloworld.Test.test
test_helloworld.Test.test ... ok

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

OK

今度はテストランナーが見つけたテストの test_helloworld.Test.test が実行される直前にコールバックが実行されていることがわかる。

まとめ

今回は Python のテストフレームワーク nose のプラグインを書く方法について調べてみた。 nose は自作パッケージを用意して、そこにプラグインとなるクラスを定義することで挙動をカスタマイズできる。 この仕組みは nose の中でも使われていて、いくつかの組み込みプラグインが用意されている。 例えばテストのカバレッジを調べるための --with-coverage オプションも、今回調べたプラグインの機構を利用しているようだ。

「スマートPythonプログラミング」という本を書きました

表題の通り Python の本を書いたので、そのご紹介です!

どんな本なの?

ひと言で表すと、自信を持って Python を書けるようになるための本です。 スマートPythonプログラミングなので、略してスマパイって呼んでください。

こんな人におすすめ

  • なんとなく書いてなんとなく動いているコードに不安を覚えている方
  • 入門書は読み終わったけど次に何をして良いか分からない方
  • Python の初心者から中級者にステップアップしたい方
  • ベストプラクティスとかアンチパターンって言葉が好きな方

上記に当てはまる方には、本書が少しでも参考になると思います。

どんなことが書いてあるの?

自分が Python を学び始めた頃に知りたかったな〜と思う内容を詰め込みました!

本は 6 つの章に分かれています。 どんなことが書かれているか簡単に紹介します。

  • 「開発環境を整えよう」では Python の開発環境について学びます。

    • pyenv で任意のバージョンの Python をインストールします
    • virtualenv & virtualenvwrapper で独立した実行環境を作ります
    • pip でサードパーティ製のパッケージをインストールします
    • IPython を使ってスニペットの動作確認をしてみます
    • pudb でソースコードをデバッグします
  • 「読みやすいコードを書く」 では Python で読みやすいソースコードを書く方法について学びます。

    • 標準的なコーディング規約 PEP8 と、それをチェックするツールの pep8/autopep8/flake8 を使います
    • PEP8 の命名規則と、名前を使ったアクセスレベルの表現方法について知ります
    • docstring を使ったドキュメンテーションについて知ります
  • 「落とし穴を避ける」では Python でやってしまいがちな誤りについて学びます。

    • Pylint を使ってコードを静的解析して落とし穴を見つけます
    • 新旧スタイルクラスの違いとか非推奨なモジュールがあることなんかを知ります
    • ※ この章にかぎらず「ここに注意!」みたいなのをこの本にはいっぱい書いてます
  • 「Python らしく書く」では Python らしいソースコードを書く方法について学びます。

    • イテレータ
    • ジェネレータ
    • 内包表記 (リスト内包、集合内包、辞書内包、ジェネレータ式)
    • デコレータ
    • コンテキストマネージャ
    • 抽象基底クラス (abc)
    • 特殊メソッド
  • 「テストを書く」では Python のソースコードをテストする方法について学びます。

    • FizzBuzz に標準ライブラリの unittest でテストを書きます
    • テストしにくいところを unittest.mock でモックに入れ替えます
    • coverage でテストのカバレッジ (網羅率) を調べます
    • テストランナー (nose) とデバッガ (pudb) を連携させます
  • 「使いやすいコードを書く」では Python で保守しやすいソースコードを書く方法について学びます。

    • setuptools で自作パッケージをパッケージングする方法を知ります
    • Python 2.x と 3.x の違いと、両方で動作するコードの書き方について知ります
    • 何気に一番ここが濃いかもしれません

この本で使う環境

登場するソースコードは Python 3.5 & 2.7 対応です。 OS には Ubuntu 14.04 LTS を使うことを想定しています。 ※ 付録に仮想マシンを使ったセットアップ方法を載せています

執筆のきっかけ

Python を良い感じに書くための知見が一通り揃っている本が書きたいなーと思ったのがきっかけです。 個人的には「エキスパートPythonプログラミング」をすごくリスペクトしています。 この本は Pythonista になりたいなら絶対に読んでおいた方が良いと思える本です。 ただ、ちょっと難易度が高いのと、発売から日が経って内容が古くなりつつありました。 そんなとき、ちょうど Kindle で電子書籍を出版できることを知ったのもあって書き始めました。

エキスパートPythonプログラミング

エキスパートPythonプログラミング

  • 作者: Tarek Ziade,稲田直哉,渋川よしき,清水川貴之,森本哲也
  • 出版社/メーカー: KADOKAWA/アスキー・メディアワークス
  • 発売日: 2010/05/28
  • メディア: 大型本
  • 購入: 33人 クリック: 791回
  • この商品を含むブログ (90件) を見る

話がちょっと脱線しますけど本書に近い性格を持った本として「Effective Python」の和訳が最近出ました。 こちらは、より純粋に Python の細かな書き方にフォーカスしている感じです。 それに比べると、本書はツールとかを含めた Python の開発全般について書いています。

Effective Python ―Pythonプログラムを改良する59項目

Effective Python ―Pythonプログラムを改良する59項目

技術的な側面について

この本はすべて Python 製のドキュメンテーションツールの Sphinx を使って書きました。 そこで得られた経験は SphinxCon JP 2015 で発表したりもしてます。

speakerdeck.com

謝辞

Sphinx で書いた本を KDP で販売するという面では @r_rudi さんの「入門Ansible」という先駆者がありました。 これは色々な面で参考にさせて頂きました。

そして、本の表紙を書いてくれた Asano Sonoko さんと本書のレビューをして下さったかわたろ先輩 (@kawatarolab) には感謝してもしきれません。

おわりに

空き時間にチマチマと書き進めていたら 1 年半くらいかかってしまいました。 その分だけ内容は詰められたんじゃないかと思います。 含める内容については何度か見直しを入れて、本当に必要そうなものだけに絞りました。 そういう意味では、何を書くかよりも何を書かないかを重要視した本かもしれません。

スマパイをよろしくお願いします!

追記: ありがとうございます

Python コミュニティで有名な @shimizukawa さんに読んで頂けました! 一点ご指摘頂いた箇所についても、現在は修正した版になっています。 むしろ、それ以外に気になる点が無かったというところにすごく安心しました…。

ありがたいことに id:methane さんのブログで紹介して頂きました! 例として他のプログラミング言語を使ったことのある人が Python のオンラインチュートリアルと本書を組み合わせて学習することをおすすめされています。 これは私の中で他のプログラミング言語に慣れ親しんだ方を読者イメージとして持っていたので、仰る通りだと思います。 そういった方に、業務に耐えるレベルのソースコードを書くための近道を作れたら良いなと考えています。

methane.hatenablog.jp