TDD Advent Calendar 2013 3日目: vim と py.test で TDD

えせ (vimmer|Pythonista) の comutt です。

TDD Advent Calendar 2013 3日目です。

3日目担当なのに、日付は4日になって6時間ほど経過しています。ごめんなさい。 記事投稿日は詐称してます!

vim と py.test で TDD

vim と py.test で TDD する方法を書いてみます。

内容はかなり手抜き気味なので突っ込みどころ満載なのですが、お許しを!

使うもの一覧

  • vim
    • +python3 feature 付きでコンパイルされた vim
  • py.test
    • Python 用のユニットテストフレームワークの一つです
  • PyHamcrest
    • Python 用の Hamcrest ライクなアサーションライブラリです
  • pytest.vim
    • py.test を vim から使うための vim プラグインです
  • pyflakes-vim
    • vim 上で Python コードを on-the-fly でコンパイルしてエラー箇所を表示してくれる vim プラグインです
    • 本家のは Python3 に対応していないので、対応版を comutt/pyflakes-vim に置いてあります
      • 2013/12/04(つまり書いてる今日)にプルリクエストを出してみたので、そのうち取り込まれるかもしれません

セットアップ

vim

+python3 または +python3/dyn feature がバンドルされていることを確認してください。

vim --version | grep python

+python でもいいのですが、 Python2 はメソッド名に日本語が使えないので可能なら Python3 を使いましょう。

ソースからコンパイルする場合は、 --enable-python3interp (+python3/dyn の場合は --enable-python3interp=dynamic) オプション付きで configure した上でコンパイルします。

ちなみに、おそらく私の環境の問題ではありますが、 MacOS X 10.7 上で --enable-python3interp=dynamic でコンパイルされた vim で python3 コマンドを使おうとすると、segfault でクラッシュしてしまいました。

プラグインは好きな方法で入れて下さい。

ここでは、 neobundle.vim を使った方法で説明します。

py.test と PyHamcrest

pip でインストール

pip install pytest PyHamcrest

pytest.vim と pyflakes-vim

.vimrc に以下の行を追加します

""" pyflakes-vim
""" 本家のは Python3 に対応していないため、 comutt/pyflakes-vim を使います
NeoBundle "comutt/pyflakes-vim", "python3-support"
let g:pyflakes_python_version = 3

""" pytest.vim
NeoBundle "alfredodeza/pytest.vim"

vim の設定

pytest.vim のコマンドでよく使うもののキーバインドを設定しておきます。

以下は私が使っている設定です。

""" Binding for Pytest
" ファイル全体に py.test を実行
nmap <silent><Leader>pf <Esc>:Pytest file<CR>
" フォーカスのあたってるクラスに py.test を実行
nmap <silent><Leader>pc <Esc>:Pytest class<CR>
" フォーカスのあたってるメソッドに py.test を実行
nmap <silent><Leader>pm <Esc>:Pytest method<CR>
" エラーメッセージなどのログウィンドウ表示をトグル
nmap <silent><Leader>ps <Esc>:Pytest session<CR>

実際にやってみる

ディレクトリ構成

以下のようなディレクトリ構成でやってみます。

python-helloworld/
├── classes
│   ├── __init__.py
│   ├── helloworld.py
│   └── tests
│       ├── __init__.py
│       └── test_helloworld.py
└── helloworld.py

py.test は __init__.py が無いディレクトリをパッケージルートディレクトリとみなすため、必ず __init__.py を適切に配置する必要があります。

ソースコード

プロダクションコード: helloworld.py

# helloworld.py
#!/usr/bin/env python3

class HelloWorld:
    def greet(self):
        return "Hello, World!"

テストコード: test_helloworld.py

import pytest
from hamcrest import *

from classes.helloworld import HelloWorld

class TestHelloWorld:

    @pytest.fixture
    def sut(self):
        return HelloWorld()

    def testGreet_HelloWorldメッセージが返ること(self, sut):
        expected = "Hello, World!"
        actual = sut.greet()

        assert_that(actual, equal_to(expected))
        # PyHamcrest を使わずに assert する場合は以下のように書く
        assert actual == expected

とりあえずテストを実行する

テストコードの説明は後に回して、テストコードを実行してみましょう。

テストコードの実行は、テストコードファイルを開いた状態で、 :Pytest file, :Pytest class, :Pytest method などを使って実行します。

:Pytest class を使うときは対象のクラススコープ内にカーソルがある状態で実行し、 :Pytest method はメソッドスコープ内にカーソルがある状態で実行します。

ただし、上記のようにテストコードにメソッド名に日本語を含む場合、pytest.vim プラグインがうまくメソッド認識をしてくれず、 :Pytest method では実行できません。

実行すると、以下のにグリーンバーが表示されます。

f:id:comutt:20131204064337p:plain

失敗した場合は以下のように表示されます。

f:id:comutt:20131204064539p:plain

:Pytest session を実行すると、エラー内容の詳細を知ることができます。(再度 :Pytest session を実行すると閉じる)

f:id:comutt:20131204064621p:plain

py.test を使ってみて便利だと思うところ

それほど使い込んでるわけではないのですが、以下の点が便利だと思うところです。

(1) @pytest.fixture デコレータでテストフィスクチャを作成できる

@pytest.fixture
def sut(self):
    return Testee()

def test_XXX(self, sut):
    # some setup, exercises, asserions...
    sut.exercise()

@pytest.fixture デコレータを使ったメソッドは各テストメソッド実行前にフィクスチャ生成処理が実行され、 テストメソッドの引数(デコレータを使ったメソッド名と同名の引数)にインスタンスが渡されます。

unittest.TestCase を使う場合、私は以下のように書いていたのですが、 @pytest.fixture を使う方が私は self. を書かなくていいのもあって好きです。

def setUp(self):
    self._sut = Testee()

def test_XXX(self):
    # some setup, exercises, asserions...
    self._sut.exercise()

(2) tmpdir が便利

tmpdir が便利すぎます。手放せません。

tmpdir が使いたいがために、 py.test を使ってると言ってもいいくらいです。

たとえば、準備処理でディレクトリを作成してファイルを書き込む、というような場合以下のように書きます。

def test_XXX(self, sut, tmpdir):
    dataDirectory = tmpdir.mkdir('data')
    sampleFile = dataDirectory.join("sample.txt")
    sampleFile.write("First line\nSecond line\n")

こうすることで、システムのテンポラリディレクトリに pytest-XXX (XXXはテスト実行ごとに採番されるシーケンスナンバー)というディレクトリが作成され、そこをルートディテクトリとしてテンポラリディレクトリ・ファイル作成がされます。

pytest-XXX ディレクトリはある程度(直近数回分*1)の数だけ保持され、自動的に古い物から削除されます。楽ちん!

おわりに

突然おわりかよ、と思われた方すみません。

ここまで書いて力尽きてしまいました。

TDD とタイトルに入れるからには、本来は jedi-vim などのリファクタプラグインの紹介もするべきでしたが、私自身がまだ使いこなせてないという問題があり、省略しました。

また、 Quick JUnit のようにテストケース<->プロダクションコードの簡単なスイッチができると俄然 TDD がすすみます。

プラグインがあるはずと調べてみたのですが、うまいこと使えるものを見つけられず、現在はゴミみたいな vimscript で切り替えています。 どなたか良いプラグインがあったら教えてください!

*1:すみません、正確な保持数は調べていません