flaskでWebアプリ開発のチュートリアルを勉強しています。今回は、「Test coverage」のブログ機能をテストする部分を進めていきます。
こんな人の役に立つかも
・PythonでWebアプリケーションを作成したい人
・flaskのチュートリアルを行なっている人
・flaskのチュートリアルでブログアプリflaskrを作成している人
ブログ機能の全体像
テストプログラムを作成する前に、ブログ機能について復習したいと思います。
以前作成したブログ機能「blog.py」には次のような機能がありました。
・「/」にアクセスしたときの「index()」関数の実行
・「/create」にログイン状態でアクセスしたときの「create()」関数の実行。GETとPOSTの2つの挙動が存在しています。
・「get_post」関数での記事が存在するか否かの条件判断
・「/記事ID/update」にログイン状態でアクセスしたときの「update(id)」関数の実行。GETとPOSTの2つの挙動があります。
・「/記事ID/delete」にログイン状態でアクセスしたときの「delete(id)」関数の実行。
この機能が網羅的にテストできるようにチュートリアルのテスト用プログラムが進んでいくようです。
自分のアプリの場合、テスト項目のリストを作るのがいいかもしれませんね〜。
ブログ機能のテスト
「tests」フォルダ内の「test_blog.py」に以下のプログラムを作成していきます。
indexページのテスト
まずは、indexページのテストプログラムです。
import pytest
from flaskr.db import get_db
#indexページのテストです。
#fixture「client」と「auth」を引数に取ります。
def test_index(client, auth):
response = client.get('/')
assert b"Log In" in response.data
assert b"Register" in response.data
#「auth」fixtureでログインします。
auth.login()
response = client.get('/')
assert b'Log Out' in response.data
assert b'test title' in response.data
assert b'by test on 2018-01-01' in response.data
assert b'test\nbody' in response.data
assert b'href="/1/update"' in response.data
まず、「client」fixtureでGETリクエストを投げます。「response」に「Log In」と「Register」の文字が存在しているかという点を確認しています。実際にWebアプリで見ると次のようなHTMLの内容に文字列が含まれているかどうかを確認していることがわかります。
そして、「auth」fixtureを使い、「test」ユーザーでログインを行います。ここでも、文字列がresponseに含まれているかをチェックしています。ログインを行った時に投稿内容が表示されているかを確認しています。(記事はテスト用にdata.sqlで追加してものが表示されるはずなので、その内容になっているかをチェックしています。)
エラー関連のテスト
ブログ機能で、まずはエラーがしっかり出るかという点を押さえておきます。
①ログインせずに各種viewにアクセスして、ログイン画面にリダイレクトされるか。
②ユーザーの記事ではない記事をアップデート、削除しようとしたときに403が返されるか。
③存在しない記事IDのアップデート、削除をしようとしたら404が返されるか。
という点のテストを行います。
#①ログインしていないとログイン画面にリダイレクトされることを検証します。
@pytest.mark.parametrize('path', (
'/create',
'/1/update',
'/1/delete',
))
def test_login_required(client, path):
response = client.post(path)
assert response.headers['Location'] == 'http://localhost/auth/login'
#②記事の作者を変更して、エラーが出るかを検証します。
def test_author_required(app, client, auth):
#記事の作者「author_id」を変更します。
with app.app_context():
db = get_db()
db.execute('UPDATE post SET author_id = 2 WHERE id = 1')
db.commit()
#実際にエラーが出るかを検証します。
auth.login()
#記事のアップデート、削除は403が返されるはずです。
assert client.post('/1/update').status_code == 403
assert client.post('/1/delete').status_code == 403
#「edit」のリンクも表示されないはず。です
assert b'href="/1/update"' not in client.get('/').data
#③存在しない記事IDのアップデートと削除は404が返されることを検証します。
@pytest.mark.parametrize('path', (
'/2/update',
'/2/delete',
))
def test_exists_required(client, auth, path):
auth.login()
assert client.post(path).status_code == 404
parametrizeデコレータで「’path’,(‘①’,’②’)」のようにすると、指定した引数(今回はpath)を複数回投げることもできるようです。
parametrizeは便利なテスト用のデコレータですね。
ログイン状態のテスト
記事の作成・アップデートの検証
#①記事作成の検証を行います。
def test_create(client, auth, app):
auth.login()
assert client.get('/create').status_code == 200
client.post('/create', data={'title': 'created', 'body': ''})
with app.app_context():
db = get_db()
count = db.execute('SELECT COUNT(id) FROM post').fetchone()[0]
assert count == 2
#②記事のアップデートの検証を行います。
def test_update(client, auth, app):
auth.login()
assert client.get('/1/update').status_code == 200
client.post('/1/update', data={'title': 'updated', 'body': ''})
with app.app_context():
db = get_db()
post = db.execute('SELECT * FROM post WHERE id = 1').fetchone()
assert post['title'] == 'updated'
#③記事作成フォーム、またはアップデートフォームの入力検証を行います。
@pytest.mark.parametrize('path', (
'/create',
'/1/update',
))
def test_create_update_validate(client, auth, path):
auth.login()
response = client.post(path, data={'title': '', 'body': ''})
assert b'Title is required.' in response.data
①、②共に、「auth」fixtureでテストユーザーにてログインを行い、それぞれGETリクエストを投げて検証を行います。DBへの接続では、アプリケーションコンテキスト内でアクセスする必要がありますので、with構文の「app.app_context」を利用していますね。
③では、先ほども利用したデコレータ「parametrize」でpath引数に2つの引数を連続的に与えることができます。記事作成画面と、アップデート画面では「タイトル」フォームと「内容」フォームが共通の形ですので、一つの関数でテストできています。今回、このテスト関数で入力がない時にちゃんとエラー文字「Title is required」が出るか、という点を検証していますね。
ここまでくるとほぼパターン的なものが見えてきましたね〜
記事の削除検証
最後に、記事の削除がしっかりできるかどうかを検証します。
#記事の削除操作を検証します。
def test_delete(client, auth, app):
auth.login()
#削除を行うview関数を呼び出します。
response = client.post('/1/delete')
#削除後、localhostにリダイレクトしているか確認します。
assert response.headers['Location'] == 'http://localhost/'
#実際にデータベースに記事IDがないことを確認します。
with app.app_context():
db = get_db()
post = db.execute('SELECT * FROM post WHERE id = 1').fetchone()
assert post is None
削除のview関数を呼び出したら、アプリケーションコンテキスト内でデータベースにアクセスし、記事データが削除されているかも確認しています。
テストをプログラム化することで、データベース内のデータの状態も確認することができますので、その点はしっかりテストできます。