ただ、前提条件があります。前回の『テストの必要性』でも説明しましたが、今回のテストコードの目的は「品質向上のためのコード」でも「TDD(テスト駆動開発)のためのコード」でもありません。
目的は、コンパイルチェックの代わりとなる「構文チェック」のためのテストコードです。このため最低限のコードを実行するだけのテストになります。しかし今回説明するコードは、より複雑な目的のためのコードにも利用が可能です。
web2pyコントローラのコードを実行するPythonシェル
doctestに記述するコードは、
- Pythonシェルで実行する。
- 実行したコマンドとその結果をコピーして、テスト対象の関数のドキュメント文字列などに貼り付ける。
web2pyでテストコードを作成するためには、コントローラコードが実行可能な Python シェルを立ち上げる必要がある。コントローコードの実行可能シェルは、次のようにオプション S の後に、アプリケーション名と実行したい関数が記述されたコントローラファイル名を付けて起動する。
C:\Users\xxx\web2py>python web2py.py -S myapp/default -M
上記例では、myapp アプリケーションの default コントローラファイルを指定して起動している。
また、このままだとインタラクティブ・シェル(IPython)が立ち上がる。しかし、インタラクティブ・シェルのレスポンスと通常のPythonシェルのレスポンスとでは、返ってくる内容が違う場合がある。このため、オプション P を付けて通常のシェル環境を起動させるようにする。
C:\Users\xxx\web2py>python web2py.py -S myapp/default -MP
起動したPythonシェルで下のように、コントローラの関数に対するテストコードを、入力して実行できることが確認できる。
C:\Users\xxx\web2py>python web2py.py -S myapp/default -MP web2py Web Framework Created by Massimo Di Pierro, Copyright 2007-2011 Version 1.99.7 (2012-03-04 22:12:08) stable Database drivers available: SQLite3, pymysql, pg8000, IMAP Python 2.7.3 (default, Apr 10 2012, 23:24:47) [MSC v.1500 64 bit (AMD64)] on win 32 Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> index().has_key('message') True >>>
この環境を使って、テストコードの作成を行っていく。
アクセス制御が設定されている関数に対するdoctest
アクセス制御が設定されている関数に対するテストを考えてみる。例えば、次のような関数だ。
1 2 3 4 5 6 7 8 | @auth.requires_login() def index(): """ >>> index().has_key('message') True """ response.flash = "Welcome to web2py!" return dict(message=T('Hello World')) |
しかし前回紹介したコマンドラインでのテストや、Pythonシェル上でのテストコード実行では、次のようなエラーになる。
>>> index().has_key('message') Traceback (most recent call last): File "<console>", line 1, in <module> File "C:\Users\xxx\web2py\gluon\tools.py", line 2565, in f '?_next='+urllib.quote(next)) File "C:\Users\xxx\web2py\gluon\tools.py", line 70, in call_or_redirect redirect(f(*args)) File "C:\Users\xxx\web2py\gluon\tools.py", line 934, in <lambda> settings.on_failed_authentication = lambda x: redirect(x) File "C:\Users\xxx\web2py\gluon\http.py", line 128, in redirect Location=location) HTTP: 303 SEE OTHER >>>
この場合、次のコードを付加すると正常に動作させることが可能だ。
>>> bool(auth.login_bare('a@a.com', 'password')) True >>> index().has_key('message') True
ここで login_bare は、ユーザ(a@a.com)及びパスワード(password)でログインを行う。ログインに成功すると login_bare は該当ユーザの auth_user レコードを返すが、冗長なため、ここでは bool関数でラップしている。
先ほどの関数にコードを追加する。このコードでは、コマンドライン上のテストもパスすることが可能だ(ユーザ及びパスワードは事前に登録しておく必要はある)。
1 2 3 4 5 6 7 8 9 10 | @auth.requires_login() def index(): """ >>> bool(auth.login_bare('a@a.com', 'password')) True >>> index().has_key('message') True """ response.flash = "Welcome to web2py!" return dict(message=T('Hello World')) |
@auth.requires_membership や @auth.requires_permission といったデコレータを使用している場合も、ログインユーザが適切な権限を所持していれば、同様にコードを追加すればテストが可能となる。
@auth.requires_signature() が設定されている関数に対するdoctest
同じアクセス制御でも、@auth.requires_signature() デコレータを使った関数に対するテストは、もう少し複雑になる。requires_signature() の詳細はリンク先を参照してもらいたいが、一口に説明すると、認証されたリンクからのみアクセスを許可する機能だ。
1 2 3 4 5 6 7 8 | @auth.requires_signature() def index(): """ >>> index().has_key('message') True """ response.flash = "Welcome to web2py!" return dict(message=T('Hello World')) |
この場合、次のコードを追加すれば、テストをパスすることが可能だ。
>>> bool(auth.login_bare('a@a.com', 'password')) True >>> url = URL(args=request.args, user_signature=True) >>> request.get_vars._signature = url[url.find('signature=')+10:] >>> index().has_key('message') True
これは、URLから生成した認証用のハッシュ値をrequest変数に設定することにより、requires_signature()をパスすることが可能となる。また requires_signature() はログインを要求するため、login_bare()も使用している。
このコードを挿入することにより、テストコードを含めた index関数定義は次のように設定できる。
1 2 3 4 5 6 7 8 9 10 11 12 | @auth.requires_signature() def index(): """ >>> bool(auth.login_bare('a@a.com', 'password')) True >>> url = URL(args=request.args, user_signature=True) >>> request.get_vars._signature = url[url.find('signature=')+10:] >>> index().has_key('message') True """ response.flash = "Welcome to web2py!" return dict(message=T('Hello World')) |
オブジェクトを返す関数に対するdoctest
今まで辞書型データを返す関数に対しては、has_key() を利用した。もちろん、これ以外の属性などのチェックを利用するのもOKだ。それでは辞書型データを返さない関数に対するdcotestは、どう書いたら良いだろうか?。コントローラファイルに、次のような関数定義があったとする。
1 2 | def __build_table(query, columns=None): return SQLTABLE(db(query).select(), columns=columns) |
この関数に対しては、次のテストコードが考えられる。
1 2 3 4 5 6 | def __build_table(query, columns=None): """ >>> __build_table(db.auth_user) <gluon.sqlhtml.SQLTABLE object at 0x0000000003C6A780> """ return SQLTABLE(db(query).select(), columns=columns) |
しかしコードの 0x0000000003C6A780 の部分はメモリのアドレス値なので、テストの度に変更されてしまう。このため次のようにコードを変更する。
1 2 3 4 5 6 | def __build_table(query, columns=None): """ >>> __build_table(db.auth_user) #doctest: +ELLIPSIS <gluon.sqlhtml.SQLTABLE object at 0x...> """ return SQLTABLE(db(query).select(), columns=columns) |
ここで #doctest: +ELLIPSIS は、doctestのオプション ELLIPSIS の使用を指示している。ELLIPSIS は出力文字列中の任意の文字列を ... で代替する。これによりアドレス値が変更になっても、テストをパスすることができる。
また、ELLIPSIS を使う代わりに、次のように記述することも可能だ。
1 2 3 4 5 6 | def __build_table(query, columns=None): """ >>> type(__build_table(db.auth_user)) <class 'gluon.sqlhtml.SQLTABLE'> """ return SQLTABLE(db(query).select(), columns=columns) |
URL変数を使う関数に対するdoctest
1 2 3 4 5 6 7 8 9 10 11 12 | @auth.requires_signature() def user_name(): """ >>> bool(auth.login_bare('a@a.com', 'password')) True >>> url = URL(args=request.args, user_signature=True) >>> request.get_vars._signature = url[url.find('signature=')+10:] >>> user_name().has_key('name') True """ r = db.auth_user(request.args(0)) return dict(name = r.first_name + ',' + r.last_name) |
URL変数を使う関数に対しては、テストを実施すると次のようなエラーが出る場合がある。
C:\Users\xxx\web2py>python web2py.py -T myapp web2py Web Framework Created by Massimo Di Pierro, Copyright 2007-2011 Version 1.99.7 (2012-03-04 22:12:08) stable Database drivers available: SQLite3, pymysql, pg8000, IMAP ********************************************************************** File "C:\Users\xxx\web2py\gluon\tools.py", line 2571, in default.py: user_name ・・・中略・・・ File "applications\myapp\controllers\default.py", line 56, in user_name return dict(name = r.first_name + ',' + r.last_name) AttributeError: 'NoneType' object has no attribute 'first_name'
この場合次のように、requestオブジェクトに対して値を設定する必要がある(4行目)。
1 2 3 4 5 6 7 8 9 10 11 12 13 | @auth.requires_signature() def user_name(): """ >>> request.args.append(1) >>> bool(auth.login_bare('a@a.com', 'password')) True >>> url = URL(args=request.args, user_signature=True) >>> request.get_vars._signature = url[url.find('signature=')+10:] >>> user_name().has_key('name') True """ r = db.auth_user(request.args(0)) return dict(name = r.first_name + ',' + r.last_name) |
その他
web2pyでの doctest のテストコードを紹介したが、重要なことを書いてなかった。それは web2py での doctest対象はあくまでも、コントローラが対象になる。モデル定義やモジュールに対しても doctest を実施したい場合、コントローラファイル中にテストコードを記述する必要がある(モジュールでのdoctesの実行方法 を参照のこと)。
また doctest のオプションとして、ELLIPSIS を紹介した。他にも幾つか有用なオプションがあるようだ。例えば、doctest をスキップする SKIP というオプションがある。
1 2 3 4 5 6 7 8 | def __build_table(query, columns=None): """ >>> __build_table(db.auth_user) # doctest: +SKIP <gluon.sqlhtml.SQLTABLE object at 0x0000000003C6A780> >>> __build_table(db.auth_user) #doctest: +ELLIPSIS <gluon.sqlhtml.SQLTABLE object at 0x...> """ return SQLTABLE(db(query).select(), columns=columns) |
参考:Python documentation - doctest
以上で今回の記事は終わりです。構文チェックのためにも、コントローラの全ての関数に対して、テストコードを書くことが望ましいです。構文チェックが主目的の場合は、テストパターンとか難しいことを考える必要がなく、コードの最後まで実行するテストコードを書くことを心がければ良いと思います。