どこにでもいるSEの備忘録

たぶん動くと思うからリリースしようぜ

【読んでみた】テスト駆動Python

こちらの本を読んでみました。

テスト駆動Python

テスト駆動Python

読んでみた感想としては、翻訳本って感じがして、初心者には読んでいてしんどかったです。 書いてあることは正しいので、習うより慣れろ的な感じで写経して覚えようと思います。

テスト駆動開発は、一朝一夕で習得できるほど甘くないと思っていますので、まずはお作法と最近良く使っているPythonでのテスト駆動開発について勉強したいと思います。

TDD初心者の私には上の本だけではわからないことが結構あったので、下記の記事なんかも参考にさせていただきました。

qiita.com

準備

pytestのインストール

pytestをインストールします

pip3 install pytest

こんだけ。簡単でいいですね。

PYTHONHOMEの設定

Dockerで弄ってて、最初はtestスクリプトからプロダクトのファイルが見えていないエラーが出ました。 最初わかんなかったんですが、単純にPYTHONROOTの環境変数を設定してなかっただけでした。

export PYTHONHOME="プロジェクトのホームディレクトリとか"

これをいじれば、プロダクトコードを呼び出してテストできるようになりました。

使い方

基本的な使い方

テストの実行

テストの実行には、pytestコマンドを使用できます。

すべてのテストスクリプトを実行する際には、

pytest

で実行できます。

ファイルを指定するときは

pytest <実行したいテスト>.py

で実行できます。

関数まで指定したいときは

pytest <実行したいファイル>.py::<実行したい関数>

で指定できます。

assert

Pytestのassert文には式を記述することができます。

assert  a == b

式の評価値がFlaseになる場合にテストが失敗したと判断されます。

テストが失敗した際には、-vオプションをつけることで詳細が確認できます。

例外

例外のテストにはpytest.raiseが使用可能です。 例外の中身も確認できます。

with pytest.raises(例外) as excinfo:
    処理
     exc_msg= excinfo.value.args[0] 

こんな感じで使えます。

テストのグルーピング

スモークテストというのがpytestでは、使用できます。 これにより、テスト関数をグループ化でき、グループごとにテストを実行することが可能になっています。

@pytest.mark.get
def test_hoge():
    処理

こんな感じで”get”というマークを付与できました。

実行時には-mオプションを使って、マークを指定してやればそのマークだけテストされます。 複数のマークを実行したい場合には、andやorが使用できるので、それをマークと組み合わせて実行していきます。

スキップ

まだ実装されてない関数に関してテストを明示的にスキップすることが可能になっています。

@pytest.mark.skip("reason=なんでスキップするのか")
def test_hoge():
    処理

parameterize

テストケースの記述はパラメータ化すると簡単のようです。

h-miyako.hatenablog.com

@pytest.mark.parameterize('変数名', [設定値1, 設定値2,  設定値3]) 
def test_foge(変数名):
    処理

こんな感じで指定した変数名に対して、変数のパラメータセットを用意できます。

フィクスチャ

フィクスチャは、実際のテスト関数の実行に先立って(またはその前後に)、pytestによって実行される関数です。

なので、pytestのフィクスチャは、世間一般のフィクスチャとは違って、テストを行う前の処理全般を指します

フィクスチャを使用する場合にはこんな感じで使用するようです。

@pytest.fixture()
def setup():
    処理

def test_hogr(setup):
    処理

__conftest.pyにフィクスチャを記述すると、すべてのテストスクリプトでフィクスチャを共有できます。 範囲指定したい場合には、

@pytest.fixture(scope=スコープ)

のようにしてスコープ指定をすることもできます。

その他、pytestライブラリにもともと組み込まれている便利fixtureもあるそうです。

  • tmpdir/tmpdir_factory
  • pytestconfig
  • cache
  • capsys
  • monkeypatch
  • doctest_namespace
  • recwarn

この辺は使う時にまた勉強します。

チュートリアルを眺める

内容が実践的な話なんで、習うより慣れろですね。 あまり参考文献がないんですが、Flaskを使用したときのテストを見てみます。

公式のFlaskチュートリアルでも、テストライブラリはpytestになっていたのでこれを使うのが標準ということなんでしょう。(以前のバージョンではunittestでした)

flask.pocoo.org

チュートリアルとgithubの内容が一致していないので、githubのコードをもとに中身を見ていきます (解説全部読めば一致するのかもしれないですが、この章だけ読むと一致しない…)

ディレクトリ構成とテスト対象のコードはこんな感じです。

github.com

fixture

Flaskのテストを行うときは、まず最初にfixtureを使用してテスト用にアプリケーションを設定します。

fixtureがconftest.pyに記述されています。

@pytest.fixture
def app():
    """Create and configure a new app instance for each test."""
    # create a temporary file to isolate the database for each test
    db_fd, db_path = tempfile.mkstemp()
    # create the app with common test config
    app = create_app({
        'TESTING': True,
        'DATABASE': db_path,
    })

    # create the database and load test data
    with app.app_context():
        init_db()
        get_db().executescript(_data_sql)

    yield app

    # close and remove the temporary database
    os.close(db_fd)
    os.unlink(db_path)

'TESTING'の設定値を有効化することで、リクエスト処理中のエラーキャッチが無効になり、アプリケーションに対してテストリクエストを実行しできるようになります。

yield appの前がテストの前処理、あとがテストの後処理となっており、SQLiteに関してdb_fdというハンドルを前処理で取得し、dbを後処理で破棄しています。

こんな感じで、アプリケーション全体のテストの前処理はconftest.pyに記述するのが基本みたいですね。

テストはこんな感じで実行できます。

flask $pytest

テクニック的な話

リクエストパラメータ

リクエストパラメータの中身はこんな感じで、wtih文とtest_clientを組み合わせて確認できるようです。

app = flask.Flask(__name__)

with app.test_client() as c:
    rv = c.get('/?tequila=42')
    assert request.args['tequila'] == '42'

セッション

セッションはこんな感じで取得可能です。

with app.test_client() as c:
    rv = c.get('/')
    assert flask.session['foo'] == 42

Json

Jsonも簡単に使用できます。

from flask import request, jsonify

@app.route('/api/auth')
def auth():
    json_data = request.get_json()
    email = json_data['email']
    password = json_data['password']
    return jsonify(token=generate_token(email, password))

with app.test_client() as c:
    rv = c.post('/api/auth', json={
        'username': 'flask', 'password': 'secret'
    })
    json_data = rv.get_json()
    assert verify_token(email, json_data['token'])

その他、もうちょっと書いてありますが、Flask関係はこんな感じでいけそうですね。

実行

まあこんな感じです。

tutorial $pytest -v
============================================================================= test session starts =============================================================================
platform darwin -- Python 3.6.4, pytest-3.3.2, py-1.5.2, pluggy-0.6.0 -- /anaconda3/bin/python
cachedir: .cache
rootdir: /home/flask/examples/tutorial, inifile: setup.cfg
collected 24 items

tests/test_auth.py::test_register PASSED                                                                                                                                [  4%]
tests/test_auth.py::test_register_validate_input[--Username is required.] PASSED                                                                                        [  8%]
tests/test_auth.py::test_register_validate_input[a--Password is required.] PASSED                                                                                       [ 12%]
tests/test_auth.py::test_register_validate_input[test-test-already registered] PASSED                                                                                   [ 16%]
tests/test_auth.py::test_login PASSED                                                                                                                                   [ 20%]
tests/test_auth.py::test_login_validate_input[a-test-Incorrect username.] PASSED                                                                                        [ 25%]
tests/test_auth.py::test_login_validate_input[test-a-Incorrect password.] PASSED                                                                                        [ 29%]
tests/test_auth.py::test_logout PASSED                                                                                                                                  [ 33%]
tests/test_blog.py::test_index PASSED                                                                                                                                   [ 37%]
tests/test_blog.py::test_login_required[/create] PASSED                                                                                                                 [ 41%]
tests/test_blog.py::test_login_required[/1/update] PASSED                                                                                                               [ 45%]
tests/test_blog.py::test_login_required[/1/delete] PASSED                                                                                                               [ 50%]
tests/test_blog.py::test_author_required PASSED                                                                                                                         [ 54%]
tests/test_blog.py::test_exists_required[/2/update] PASSED                                                                                                              [ 58%]
tests/test_blog.py::test_exists_required[/2/delete] PASSED                                                                                                              [ 62%]
tests/test_blog.py::test_create PASSED                                                                                                                                  [ 66%]
tests/test_blog.py::test_update PASSED                                                                                                                                  [ 70%]
tests/test_blog.py::test_create_update_validate[/create] PASSED                                                                                                         [ 75%]
tests/test_blog.py::test_create_update_validate[/1/update] PASSED                                                                                                       [ 79%]
tests/test_blog.py::test_delete PASSED                                                                                                                                  [ 83%]
tests/test_db.py::test_get_close_db PASSED                                                                                                                              [ 87%]
tests/test_db.py::test_init_db_command PASSED                                                                                                                           [ 91%]
tests/test_factory.py::test_config PASSED                                                                                                                               [ 95%]
tests/test_factory.py::test_hello PASSED                                                                                                                                [100%]

========================================================================== 24 passed in 1.14 seconds ==========================================================================

感想

アジャイルサムライにも、何はともあれテスト駆動開発をやれって書いてあったので、TDDができるように少しずつ慣らしていきたいと思います。

pytestについては、簡単なところはわかりましたがまだまだ奥が深いと思っているので、使いながら慣れていこうと思います。 (そもそもPythonもきれいに書けないですし。。。)

それでも基本的な書き方はなんとなくわかったので、あとはささっとテストを設計できるといいですね。 今度覚えてたらテスト設計について勉強してみたいと思います。