Kensuke Kousaka's Blog

Notes for Developing Software, Service.

noseを用いたPython Flaskプログラムのユニットテスト

PythonベースのWebアプリケーションフレームワークであるFlaskを用いて開発しているWebアプリについて,その動作テストをしてみたいと思った.調べていると,Pythonユニットテストライブラリであるnoseを用いることでかなり簡単にテストコードを実装できそうだと分かり,実際にそのWebアプリにもテストを導入してテストカバレッジを94%まで上げられた.

ここではFlaskを用いたサンプルWebアプリに対して,noseを用いたユニットテストを行う方法を書こうと思う.

Flaskを用いたサンプルWebアプリの準備

この記事で作成したFlaskベースのWebアプリに対してnoseを用いたユニットテストを行っていくので,サンプルプログラムをあらかじめ用意しておく.おそらく以下のようなツリー構造になるはず.

- HelloFlask/
  - FlaskApp/
    - app.py
    - static/
      - css/
        - sample.css
      - js/
        - sample.js
    - templates/
      - index.html
      - login.html

noseを用いたユニットテスト

noseのインストール

以下のコマンドを実行し,noseをインストールする.

# pip install nose

テストの作成

はじめにテストプログラムを配置するためのディレクトリをTestsという名称でHelloFlask/の下に作成する.

- HelloFlask/
  - FlaskApp/
    - app.py
    - static/
      - css/
        - sample.css
      - js/
        - sample.js
    - templates/
      - index.html
      - login.html
  - Tests/

これから書いていくテストコードはすべてTests/の下に置いていく.

さて,まずは以下のようなテストコードをtest_access.pyというファイル名で作成する.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from nose.tools import eq_, ok_
from FlaskApp import app

app.testing = True
client = app.app.test_client()


def test_get_index():
   res = client.get('/')
   eq_(302, res.status_code)
   ok_('/login' in res.headers['Location'])

app.testing = Trueの行でFlaskをテストモードを実行するように設定し,client = app.app.test_client()の行でFlaskで用意されているテスト用クライアントを取得している.テストの中身はdef test_get_index():の部分になるのだが,ここではテスト用クライアントで/,つまりhttp://127.0.0.1:8080/にアクセスしてその結果を受け取り,その下のeq_行とok_行でそれぞれ期待した結果を得られているかをテストしている.今回テスト対象として用いているサンプルアプリでは,/loginページでの認証がなされない状態で/にアクセスすると/loginにリダイレクトさせるように実装している.そのためテストコードでは/にアクセスした際にステータスコードでリダイレクトを示す302が返ってきているか,リダイレクト先のURLとして/loginを含むURLが指定されているかを確認している.

テストコードを実装できたらプロジェクトのルートディレクトリ,つまりHelloFlask/の下に移動し,そこでnosetestsコマンドを実行してみよう.すると以下のような出力が得られるはずだ.

.
----------------------------------------------------------------------
Ran 1 test in 0.080s

OK

nosetestsコマンドを実行すると,カレントディレクトリ以下からtestTestを含むファイルを探してそれをテストプログラムとして認識し,プログラム内でtestTestから始まっているメソッドをテストコードとして実行する.上の出力ではテストコードを一つ実行し,テストに成功したと結果を伝えている.

次に,わざと失敗するテストコードを書いてみよう.先ほど作成したtest_access.pyに新たに以下のコードを追加する.

def test_fail_get_index():
  res = client.get('/')
  eq_(200, res.status_code)

コードを追加できたら,プロジェクトのルートディレクトリに移動して改めてnosetestsを実行してみよう.先ほどとは違った出力が得られるはずだ.FAIL: test_access.test_fail_get_indexという出力,Traceback,AssertionError: 200 != 302という出力,そしてFAILED (failures=1)という出力があるはず.これら出力の意味するところはtest_access.pytest_fail_get_indexメソッドについて,Tracebackで表示された部分でAssertionError: 200 != 302があったということである.

その上のメソッドを実装する際に説明した通り,/loginでの認証が行えていないため,/にアクセスしても/loginにリダイレクトされる.そのためステータスコード200ではなく302が返るのでeq_(200, res.status_code)は成立せず,AssertionErrorになるといった具合である.結果を得られたら,この失敗するテストコードは削除しておこう.

削除できたら,test_access.pyを以下のように編集しよう.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from nose.tools import eq_, ok_
from FlaskApp import app
import json

app.testing = True
client = app.app.test_client()


def test_get_index():
   res = client.get('/')
   eq_(302, res.status_code)
   ok_('/login' in res.headers['Location'])


def test_get_login():
   res = client.get('/login')
   eq_(200, res.status_code)


def test_fail_login():
   res = client.post('/login', data={
       'username': 'testuser',
       'password': 'testpassword'
   })
   eq_(200, res.status_code)


def test_login():
   res = client.post('/login', data={
       'username': 'admin',
       'password': 'testpassword'
   })
   eq_(302, res.status_code)


def test_post_hoge():
   res = client.post('/postText',
                     data=json.dumps(dict(text='hoge')),
                     content_type='application/json')
   eq_(200, res.status_code)
   data = json.loads(json.loads(res.data.decode('utf-8'))['ResultSet'])
   eq_('hoge', data['result'])


def test_post_HOGE():
   res = client.post('/postText',
                     data=json.dumps(dict(text='HOGE')),
                     content_type='application/json')
   eq_(200, res.status_code)
   data = json.loads(json.loads(res.data.decode('utf-8'))['ResultSet'])
   eq_('hoge', data['result'])


def test_post_ping():
   res = client.post('/postText',
                     data=json.dumps(dict(text='ping')),
                     content_type='application/json')
   eq_(200, res.status_code)
   data = json.loads(json.loads(res.data.decode('utf-8'))['ResultSet'])
   eq_('pong', data['result'])


def test_logout():
   res = client.get('/logout')
   eq_(302, res.status_code)

このテストコードにより,認証の機能,テキスト送信・小文字変換・ping-pong機能,ログアウト処理がうまく動いているかを確認している.テストを行う場合は,先ほどまでと同じくプロジェクトのルートディレクトリに移動し,nosetestsを実行すればいい.テストコードの内容については,ぱっと見で大体理解できるのではないかと思う.また,noseを用いたユニットテストの実装がいかに簡単かがわかるだろう.

このサンプルアプリはGitHubに公開している.