テスト全体の概要も理解できましたので、実際にテストを行うプログラムを見ていきます。
見た感じ、たくさんのテストプログラムがありますね〜
前回に、テストの全体像を掴みましたので、これから具体的にテスト項目のプログラミングに入っていきます。今回は、プログラムが出来るだけ何をしているのか書き出したかったですので、文字量多めで申し訳ないです。
公式ドキュメントのチュートリアルは次のページになります。
こんな人の役に立つかも
・PythonでWebアプリケーションを作成したい人
・flaskのチュートリアルを行なっている人
・flaskのチュートリアルでブログアプリflaskrを作成している人
Factoryのテスト
pytestでのテストは、test_●●という関数自体が一つのテストになります。プログラムから、Factoryでは2つのテストが実施されることがわかります。
Factory自体は、テストが始まる際に必ず作成されるので、あまりテストすることがないとのことです。ここでは、アプリケーションファクトリが正常に動いているかどうかを検証する、という言い方が近いでしょうか。
「tests」フォルダ内に次のプログラム「test_factory.py」を作成します。(ファイル名に「test_」とつけることが大事ですね。)
#アプリケーションファクトリのcreate_appをimport
from flaskr import create_app
#①テストモードであるかどうかをテスト
#「app」作成過程の検証:テストモードになるかどうか
def test_config():
assert not create_app().testing
assert create_app({'TESTING': True}).testing
#②ルーティング「/hello」をアクセスしてみる
#fixtureのclientを関数の引数として取ることで「client」を利用可能に
def test_hello(client):
response = client.get('/hello')
assert response.data == b'Hello, World!'
Factoryでは次の2点をテストしています。
①「test_config」:テストモードであるかどうかの確認。assertは、後ろの条件がTrueでなければ例外を返します。「create_app」の引数に何も指定せずアプリケーションファクトリでアプリを起動すると、テストモードのtestingは「False」となるはずです。そして、notで反転させていますので、アプリがテストモードでない場合「True」となるような条件となります。
次に、create_appにTESTING「True」をいれると、テストモードで起動しますので、「.testing」はTrueとなるはずです。
「test_config」のテスト用関数ではこのような動作を確認しています。
②「test_hello」:次のtest_helloでは、アプリケーションファクトリのcreate_app関数でアプリを起動すると、ルーティングとして「/hello」が定義されるようにしていました。このHelloにGETリクエストを送信してみて、レスポンスに「b’Hello, World!’」が返ってくるかどうかを検証しています。
ここでは、ブラウザではない、fixtureで定義したクライアントを利用してGETリクエストを送信できる、という点が重要ですね。
Factoryはまあ、このくらいのテストかと思われます。
Databaseのテスト
次に、データベースのテストです。データベースのテスト用プログラムは、「tests」フォルダ内「test_database.py」として作成します。
「db.py」には、get_db()、close_db()、init_db_command()という関数を作成しましたので、これらの関数がアプリの処理の流れの中で正しく動作しているかを確認していくようです。(init_dbはinit_db_commandから呼び出される)
import sqlite3
import pytest
from flaskr.db import get_db
#fixture「app」を引数とします。
def test_get_close_db(app):
#①アプリケーションコンテキストを手動で使い、DB接続テストを行います。
with app.app_context():
db = get_db()
#isで同一のオブジェクトかどうかを比較する
assert db is get_db()
#②例外を発生させるテスト
with pytest.raises(sqlite3.ProgrammingError) as e:
db.execute('SELECT 1')
assert 'closed' in str(e.value)
①では、get_dbを確認して言います。get_db関数の中では、アプリケーションコンテキストという概念に所属する「current_app」を利用しているので、アプリケーションコンテキストの中でget_dbを呼び出す必要があります。アプリケーションコンテキストを手動で利用するために「with app.app_context():」としています。アプリケーションコンテキストという概念は、クライアントがサーバーなどにリクエストを投げて、アプリが何かしらの動作を始めたらアプリケーションコンテキストは開始され、アプリの設定情報など(current_appとgが利用できるようになります。)を保管しておくような仕組みです。基本的に、リクエストが終了したらアプリケーションコンテキストも終了するようです。
(チュートリアルでコンテキストについて別途理解を深めています。わかりみがまだ浅いです・・・)
とりあえず、本番ではクライアントであるブラウザからリクエストを投げることでアプリケーションコンテキストは自動で生成されたり消滅したりしてcurrent_appやgが利用できるようになるのですが、今回はテストなので、アプリケーションコンテキストも手動で制御して動作を確認しなければいけません。
アプリの動作を確認するために、アプリケーションコンテキストをこのように立ち上げる、という点が必要になってきます。
アプリケーションコンテキストをwith文で立ち上げて、その中では、get_db関数を何回投げても同一のデータベースを示すかどうか、をテストしています。オブジェクトのidを比較するときには「==」ではなく「is」で比較しますので、isが使われているんですね。
②では、close_db()が正常に動作しているかを確認するため、「pytest.raises」で例外を発生させています。例外を発生させたとき(何かしらのエラーでアプリが停止したとき)にデータベースがしっかりとコネクションをclose_dbで閉じてくれるかを確認しているんですね。
SQLite3のProgrammingErrorという例外を発生させて、例外で発生したメッセージを「e」で呼び出せるようにしています。pytestのraiseもwith構文で利用します。
最後に、eに取得したメッセージ内に「closed」という文字が含まれているかを確認します。例外が発生してデータベースのコネクションが閉じられているかを確認しています。
db.pyも見ながらテストプログラムを眺めてみると良いです。
こう書くというルールが多いよね。
続いて、データベースの初期化に関するコマンドのテストです。アプリを立ち上げるときに、データベースの初期化を「flask init-db」としていましたが、それを行うテストプログラムです。
def test_init_db_command(runner, monkeypatch):
class Recorder(object):
called = False
def fake_init_db():
Recorder.called = True
#①pytestのmonkeypatchという機能でinit_dbを上のfake_init_dbに置き換えます。
monkeypatch.setattr('flaskr.db.init_db', fake_init_db)
#②コマンドを実行できるfixture、runnnerでinit-dbコマンドを投げます。
result = runner.invoke(args=['init-db'])
#③runnnerの返り値であるresultの中身はoutputで確認できます。
assert 'Initialized' in result.output
assert Recorder.called
①では、pytestの置き換え機能であるmonkeypatchというものが利用されています。init_dbの処理は、db.pyでSQL文を実行するように作成されていましたが、monkeypatchによってfake_init_dbという関数(テスト用のRecorderクラスのCalledをTrueに変更する)に置き換えています。
②で、コマンドを実行するfixture、runnnerを利用してinit-dbコマンドを投げています。これは、ターミナルで「flask init-db」とするのと同じ動作をプログラムで行なっています。
③で、コマンドから帰ってきた結果を取り出して、その中に、「Initialized」という文字列が含まれていたらデータベースが初期化された処理が全て正常に実行されていることが確認できます。
また、init_dbが呼び出されたかどうかは、monkeypatchで置き換えられたfake_init_dbが実行されていれば良いので、最後にRecorderクラスのCalledがTrueに書き換えられていることをassertで確認しています。
テストも複雑・・・