テストツール¶
Django は、テストを書くのに便利なツールをいくつか提供しています。
テストクライアント¶
テストクライアントは、ダミーのウェブブラウザとして振る舞う Python のクラスです。これを使えば、ビューと Django で作ったアプリケーションとの関係をプログラムから自動でテストできるようになります。
テストクライアントでは、次のようなことができます。
ある URL に対する GET および POST リクエストのシミュレートと、その結果の観察。低レベルの HTTP (レスポンスのヘッダーやステータスコードなど) から、ページのコンテンツまで、あらゆるレスポンスの内容を調査できます。
一連の (好きな数の) リダイレクトを見て、その各ステップごとに URL とステータスコードをチェックする。
指定したリクエストが特定の Django テンプレートによってレンダリングされたとき、テンプレートのコンテキストが正しく特定の値を含んでいるかどうかをテストする。
テストクライアントは Selenium や他の "ブラウザ内 (in-browser)" フレームワークの代替を目指すものではないことに注意してください。Django のテストクライアントの目的は別の点にあります。つまり、
Django のテストクライアントは、正しいテンプレートがレンダリングされ、そのテンプレートが正しいコンテキストデータをちゃんと渡しているのかをチェックするために使います。
ビューの機能を直接テストするには、
RequestFactoryを使用して、ルーティングとミドルウェアのレイヤーをバイパスしてください。Selenium などのブラウザー内フレームワークは、HTML の レンダリング や、ウェブページの 振る舞い 、つまり JavaScript の機能性をテストするために使います。また、Django はこれらのフレームワーク向けの特別なサポートも提供しています。詳細については、
LiveServerTestCaseのセクションを読んでください。
包括的なテストスイートを実現するには、これらすべてのタイプのテストを組み合わせて行うべきです。
概要と簡単な例¶
テストクライアントを使うには、django.test.Client からインスタンスを作り、次のようにウェブページを取得します。
>>> from django.test import Client
>>> c = Client()
>>> response = c.post("/login/", {"username": "john", "password": "smith"})
>>> response.status_code
200
>>> response = c.get("/customer/details/")
>>> response.content
b'<!DOCTYPE html...'
この例が示唆しているように、 Client のインスタンスは、Python のインタラクティブなインタプリタ上のセッションからでも作ることができます。
テストクライアントの動作の仕方に関して、いくつか大切な注意点があります。
テストクライアントが動作するために、ウェブサーバーが動作している必要は ありません 。実際、ウェブサーバーを一切起動せずに動作するのです! HTTP のオーバーヘッドを避けて、Django フレームワークと直接やりとりすることでこれを実現しています。これにより、高速なユニットテストが可能になっています。
ページの取得時には、ドメイン全体ではなく、URL の path だけを指定することを覚えておいてください。たとえば、次は正しいです。
>>> c.get("/login/")
しかし、これは間違いです。
>>> c.get("https://www.example.com/login/")
テストクライアントには、自分の Django プロジェクト以外のウェブページを取得する機能はありません。他のウェブページが必要な場合には、
urllibなどの Python のスタンダードライブラリモジュールを利用してください。URL を解決するとき、テストクライアントは
ROOT_URLCONF設定で指定されたすべての URLconf を使用します。上の例では Python のインタラクティブなインタプリタ上でも動作するはずですが、テストクライアントの一部の機能、特にテンプレート関係の機能は、 テストの実行中 にしか使えないことがあります。
というのも、Django のテストランナーは、与えられたビューによって読み込まれるテンプレートを決定する時に、ちょっとした黒魔術を使っています。この黒魔術 (具体的には Django のテンプレートシステムに対してメモリ上でパッチを当てています) は、テストの実行中にだけ使われるのです。
デフォルトでは、テストクライアントはサイト上でのすべての CSRF チェックを無効にしています。
何らかの理由でテストクライアントに CSRF チェックを実行して ほしい ときには、CSRF チェックの実行を強制するテストクライアントのインスタンスを作ることができます。これには、クライアントを作る時に次のように
enforce_csrf_checks引数を渡します。>>> from django.test import Client >>> csrf_client = Client(enforce_csrf_checks=True)
リクエストの作成¶
リクエストの作成には、django.test.Client クラスを使います。
- class Client(enforce_csrf_checks=False, raise_request_exception=True, json_encoder=DjangoJSONEncoder, *, headers=None, query_params=None, **defaults)[ソース]¶
テスト用の HTTP クライアントです。いくつかの引数で動作をカスタマイズできます。
headersにはリクエストごとに送信されるデフォルトのヘッダを指定できます。例えば、User-Agentヘッダの場合:client = Client(headers={"user-agent": "curl/7.79.1"})
query_paramsを使用すると、すべてのリクエストに設定されるデフォルトのクエリ文字列を指定できます。Defaults内の任意のキーワード引数には WSGI 環境変数 を指定します。例えば、スクリプト名の場合:client = Client(SCRIPT_NAME="/app/")
注釈
HTTP_プレフィックスで始まるキーワード引数がヘッダーとして設定されますが、可読性のためにheadersパラメーターの使用を推奨します。get()やpost()などに渡されるheaders、query_params、およびextraキーワード引数の値は、クラスコンストラクタに渡されたデフォルト値よりも優先されます。enforce_csrf_checks引数を使うと、CSRF プロテクションのテストが実行できます (上の説明を参照)。raise_request_exception引数を指定することで、リクエスト中に発生した例外をテストでも発生させるかどうかを制御できます。デフォルトはTrueです。json_encoder引数を指定すると、post()に記述されているJSONシリアライズ用のカスタムJSONエンコーダを指定できます。Changed in Django 5.1:query_params引数が追加されました。Clientインスタンスを一度作れば、以下のメソッドを自由に使うことができます。- get(path, data=None, follow=False, secure=False, *, headers=None, query_params=None, **extra)[ソース]¶
与えられた
pathに対して GET リクエストを作り、Responseオブジェクトを返します。Responseオブジェクトについては、下のセクションにドキュメントされています。query_paramsに渡された辞書のキーと値のペアは、クエリ文字列の設定に使用されます。例えば、次のように使います。>>> c = Client() >>> c.get("/customers/details/", query_params={"name": "fred", "age": 7})
引数の評価の結果、次の GET リクエストの実行と等価になります。
/customers/details/?name=fred&age=7
これらのパラメータを
dataパラメータに渡すことも可能です。しかし、query_paramsを使用する方が推奨されます。なぜなら、query_paramsはすべてのHTTPメソッドで動作するからです。headers引数は、リクエスト時に送信されるヘッダーの指定に使われます。たとえば、次のコード>>> c = Client() >>> c.get( ... "/customers/details/", ... query_params={"name": "fred", "age": 7}, ... headers={"accept": "application/json"}, ... )
は、HTTP ヘッダー
HTTP_ACCEPTを details ビューに送信します。これは、django.http.HttpRequest.accepts()メソッドを使ったコードパースのテストのための良い方法です。任意のキーワード引数には、WSGI 環境変数 を指定します。たとえば、スクリプト名を設定するヘッダの場合:
>>> c = Client() >>> c.get("/", SCRIPT_NAME="/app/")
GET の引数がすでに URL エンコードされた形式である場合は、data 引数の代わりにエンコード済みの文字列を使うことができます。たとえば、先ほどの GET リクエストは次のようにも書けます。
>>> c = Client() >>> c.get("/customers/details/?name=fred&age=7")
エンコード済みのGETデータを含むURLと
query_paramsまたはdata引数の両方を指定した場合、これらの引数が優先されます。followをTrueを与えると、クライアントはすべてのリダイレクトを辿り、途中の URL とステータスコードのタプルが、レスポンスオブジェクトのredirect_chain属性に追加されてゆきます。たとえば、URL
/redirect_me/が/next/にリダイレクトし、それがさらに/final/にリダイレクトするような場合には、redirect_chainは次のような値になります。>>> response = c.get("/redirect_me/", follow=True) >>> response.redirect_chain [('http://testserver/next/', 302), ('http://testserver/final/', 302)]
secureをTrueに設定すると、クライアントは HTTPS リクエストをエミュレートします。Changed in Django 5.1:query_params引数が追加されました。
- post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, *, headers=None, query_params=None, **extra)[ソース]¶
与えられた
pathに対して POST リクエストを作り、Responseオブジェクトを返します。Responseオブジェクトについては、下のセクションにドキュメントされています。dataディクショナリ内の key-value ペアは、POST データを送信するのに使われます。例えば、次の例では、>>> c = Client() >>> c.post("/login/", {"name": "fred", "passwd": "secret"})
引数の評価の結果、次の URL へ POST リクエストが行われます。
/login/
リクエストで送られる POST データは次のものになります。
name=fred&passwd=secret
content_typeに application/json を指定すると、dataが dict、リスト、タプルの場合、json.dumps()を使ってシリアライズされます。デフォルトではDjangoJSONEncoderでシリアライズを行いますが、Clientにjson_encoder引数を指定することでオーバーライドできます。このシリアライズはput(),patch(),delete()リクエストでも行われます。もし他の
content_type(たとえば、XML データの場合は text/xml) を与えると、dataのコンテンツは、HTTP のContent-Typeヘッダーの中でcontent_typeを使ったものとして、POST リクエストで送られます。content_typeに値を渡さなかったときは、data内の値を multipart/form-data のコンテンツタイプとして送信します。この場合は、data内のkey-value ペアが multipart メッセージにえんこーどされ、POST データを生成するのに使われます。たとえば
<select multiple>の複数の選択を指定する場合のように、特定のキーに対して複数の値を送信したいときは、必要なキーに対する値をリストまたはタプルとして与えます。たとえば、dataに次の値を与えれば、3つの選択した値をchoiceという名前のフィールドに対して送信できます。{"choices": ["a", "b", "d"]}
ファイルの送信は特別なケースです。ファイルをPOSTするには、キーとしてファイルフィールド名を、値としてアップロードしたいファイルへのファイルハンドルを渡すだけで十分です。例えば、フォームに
nameとattachmentのフィールドがあり、後者がFileFieldの場合:>>> c = Client() >>> with open("wishlist.doc", "rb") as fp: ... c.post("/customers/wishes/", {"name": "fred", "attachment": fp}) ...
また、ファイルのようなオブジェクト (例えば
StringIOやBytesIO) をファイルハンドルとして指定することもできます。もしImageFieldにアップロードする場合、オブジェクトにはvalidate_image_file_extensionバリデータを渡すname属性が必要です。たとえば下記のようにします:>>> from io import BytesIO >>> img = BytesIO( ... b"GIF89a\x01\x00\x01\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00" ... b"\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x01\x00\x00" ... ) >>> img.name = "myimage.gif"
複数の
post()の呼び出しに対して同じファイルハンドラを使う時には、post 間でファイルポインタを手動でリセットする必要があります。これを一番簡単に扱う方法は、上に示したように、ファイルがpost()に与えられた後に手動でファイルを close することです。データが読み込めるように、正しい方法でファイルを開くようにする必要があります。これはつまり、画像ファイルなどのバイナリデータが含まれている場合には、
rb(read binary、バイナリ読み込み) モードで開かなければならないということです。headers、query_params、およびextraパラメータは、Client.get()の場合と同じように動作します。POST でリクエストした URL にエンコード済みパラメータが含まれている場合には、これらのデータは request.GET データから利用できます。たとえば、次のようなリクエストを行った場合、
>>> c.post( ... "/login/", {"name": "fred", "passwd": "secret"}, query_params={"visitor": "true"} ... )
このリクエストをハンドリングするビューでは、request.POST からはユーザー名とパスワードを取得し、request.GET からはユーザーが visitor であるかどうかを特定できます。
followをTrueを与えると、クライアントはすべてのリダイレクトを辿り、途中の URL とステータスコードのタプルが、レスポンスオブジェクトのredirect_chain属性に追加されてゆきます。secureをTrueに設定すると、クライアントは HTTPS リクエストをエミュレートします。Changed in Django 5.1:query_params引数が追加されました。
- head(path, data=None, follow=False, secure=False, *, headers=None, query_params=None, **extra)[ソース]¶
指定された
pathに対して HEAD リクエストを行い、Responseオブジェクトを返します。このメソッドはClient.get()と同様に動作しますが、follow、secure、headers、query_params、およびextraパラメータを含みます。ただし、メッセージボディは返されません。Changed in Django 5.1:query_params引数が追加されました。
- options(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)[ソース]¶
与えられた
pathに対して OPTION リクエストを作り、Responseオブジェクトを返します。RESTful インターフェイスのテスト時に有用です。dataが与えられると、request body として使われます。content_typeはContent-Typeヘッダーに設定されます。follow、secure、headers、query_params、およびextraパラメータは、Client.get()の場合と同様に動作します。Changed in Django 5.1:query_params引数が追加されました。
- put(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)[ソース]¶
与えられた
pathに対して PUT リクエストを作り、Responseオブジェクトを返します。RESTful インターフェイスのテスト時に有用です。dataが与えられると、request body として使われます。content_typeはContent-Typeヘッダーに設定されます。follow、secure、headers、query_params、およびextraパラメータは、Client.get()の場合と同様に動作します。Changed in Django 5.1:query_params引数が追加されました。
- patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)[ソース]¶
与えられた
pathに対して PATCH リクエストを作り、Responseオブジェクトを返します。RESTful インターフェイスのテスト時に有用です。follow、secure、headers、query_params、およびextraパラメータは、Client.get()の場合と同様に動作します。Changed in Django 5.1:query_params引数が追加されました。
- delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)[ソース]¶
与えられた
pathに対して DELETE リクエストを作り、Responseオブジェクトを返します。RESTful インターフェイスのテスト時に有用です。dataが与えられると、request body として使われます。content_typeはContent-Typeヘッダーに設定されます。follow、secure、headers、query_params、およびextraパラメータは、Client.get()の場合と同様に動作します。Changed in Django 5.1:query_params引数が追加されました。
- trace(path, follow=False, secure=False, *, headers=None, query_params=None, **extra)[ソース]¶
与えられた
pathに対して TRACE リクエストを作り、Responseオブジェクトを返します。診断のための調査をシミュレートするときに役に立ちます。他のリクエストメソッドとは違い、
dataがキーワード引数にありません。 RFC 9110 Section 9.3.8 に従うためです。そのため、TRACE リクエストには body を含むことが禁止されています。follow、secure、headers、query_params、およびextraパラメータは、Client.get()の場合と同様に動作します。Changed in Django 5.1:query_params引数が追加されました。
- login(**credentials)¶
- alogin(**credentials)¶
非同期バージョン:
alogin()あなたのサイトが Django の 認証システム を使っていて、ユーザーのログインをテストしたければ、テストクライアントの
login()メソッドを使うことで、ユーザーがサイトにログインしたときの状況をシミュレートできます。このメソッドを呼ぶ事で、テストクライアントはログインに基づいてビューを形成するテストを行う上で必要なクッキーとセッション情報を全て持ちます。
引数
credentialsの形式は利用している (AUTHENTICATION_BACKENDSの設定値に定義されています) 認証バックエンド に依存します。Django によって提供される標準の認証バックエンド (ModelBackend) を用いている場合は、credentialsは利用者のユーザー名とパスワードであり、キーワード引数として渡されます:>>> c = Client() >>> c.login(username="fred", password="secret") # Now you can access a view that's only available to logged-in users.
もし標準以外の認証バックエンドを利用している場合、このメソッドは異なった認証情報を必要とします。この値では利用している認証バックエンドの
authenticate()メソッドによって要求される認証情報が必要になります。認証情報が受け入れられてログインが成功した場合に
login()はTrueを返します。最後に、このメソッドを使う前にユーザーアカウントを作成する必要があります。上で説明したように、テストランナーはデフォルトではユーザーを含まないテスト用データベースを使用して実行されます。その結果、本番サイトで有効なユーザーアカウントはテスト条件下では動作しません。テストスイートの一部として、手動で(Django モデル API を使って)、あるいはテストフィクスチャを使ってユーザを作成する必要があります。テストユーザーにパスワードを持たせたい場合、 password 属性を直接設定してユーザーのパスワードを設定することはできないことを覚えておいてください。
set_password()関数を使って、正しくハッシュ化されたパスワードを保存する必要があります。また、create_user()ヘルパーメソッドを使って、正しくハッシュ化されたパスワードを持つ新しいユーザーを作成することもできます。Changed in Django 5.0:alogin()メソッドが追加されました。
- force_login(user, backend=None)¶
- aforce_login(user, backend=None)¶
非同期バージョン:
aforce_login()あなたのサイトが Django の 認証システム を使っている場合、
force_login()メソッドを使うと、ユーザがサイトにログインしたときの影響をシミュレーションできます。テストがユーザのログインを必要とするが、ユーザがどのようにログインしたかの詳細は重要ではない場合にはlogin()の代わりにこのメソッドを使用してください。login()とは異なり、このメソッドは認証と確認のステップを省略します。アクティブでないユーザ (is_active=False) もログインを許可され、ユーザの認証情報を提供する必要はありません。ユーザーには、
backend引数の値(ドットで区切られたPythonパスの文字列であるべき)がbackend属性としてセットされます。値が指定されていない場合は、settings.AUTHENTICATION_BACKENDS[0]に設定されます。login()によって呼び出されるauthenticate()関数は、通常このようにユーザーにアノテーションを付けます。このメソッドは、高コストなパスワードハッシングアルゴリズムを回避するため、
login()よりも高速です。また、 テストに弱いハッシャーを使う ことで、login()処理を高速にすることもできます。Changed in Django 5.0:aforce_login()メソッドが追加されました。
- logout()¶
- alogout()¶
非同期バージョン:
alogout()あなたのサイトが Django の 認証システム を使っている場合、
logout()メソッドを使うと、ユーザがサイトからログアウトしたときの効果をシミュレートできます。このメソッドを呼び出した後、テストクライアントはすべてのクッキーとセッションデータをデフォルトにクリアします。それ以降のリクエストは
AnonymousUserから来たように見えるでしょう。Changed in Django 5.0:alogout()メソッドが追加されました。
レスポンスのテスト¶
get() および post() メソッドは、両方とも Response オブジェクトを返します。この Response オブジェクトは、Django のビューによって返される HttpResponse オブジェクトとは 異なるもの であり、テストのレスポンスには、テストコードの検証に便利な追加データがいくつかあります。
特に、Response オブジェクトは以下の属性を持ちます:
- class Response¶
- client¶
レスポンスの結果の元となるリクエストを作るために使われた、テストクライアントです。
- content¶
Bytestring としてのレスポンスの本文です。ビューないしエラーメッセージによってレンダリングされる際の最終的なページコンテンツです。
- context¶
テンプレートの
Contextインスタンスです。レスポンスの content を生成するテンプレートをレンダリングする際に使われます。描画されたページが複数のテンプレートを使っていた場合、
contextはContextオブジェクトのリストとなり、その順序はレンダリングされた順となります。レンダリングに使われるテンプレートの数にかかわらず、
[]オペレータを使ってコンテキストの値を取り出すことができます。たとえば、コンテキストの変数nameは以下のように取り出せます。>>> response = client.get("/foo/") >>> response.context["name"] 'Arthur'
Django のテンプレートを使っていない?
この属性は、
DjangoTemplatesバックエンドを使用しているときのみ格納されます。他のテンプレートエンジンを使っている場合、context_dataがこの属性を扱うレスポンスの適切な選択肢となるでしょう。
- exc_info¶
ビュー中で発生した未処理の例外 (もし存在すれば) に関する情報を提供する3つの値のタプル。
値は (type, value, traceback) であり、Python の
sys.exc_info()が返すのと同じものです。それぞれの値の意味は以下のとおりです。type: 例外の型。
value: 例外インスタンス。
traceback: 例外がもともと発生した場所におけるロールスタックをカプセル化した、トレースバックオブジェクト。
例外が発生しなかった場合、
exc_infoはNoneになります。
- json(**kwargs)¶
JSON としてパースされた、レスポンスの本文です。追加のキーワード引数が
json.loads()に渡されます。たとえば、次のようになります。>>> response = client.get("/foo/") >>> response.json()["name"] 'Arthur'
Content-Typeヘッダが"application/json"ではない場合、レスポンスをパースする際にValueErrorが投げられます。
- request¶
レスポンスを機能させるリクエストデータです。
- wsgi_request¶
レスポンスを生成したテストハンドラーによって生成された
WSGIRequestインスタンス。
- status_code¶
レスポンスの HTTP ステータスで、数値です。 定義済みのコードの全リストは、IANA status code registry を参照してください。
- templates¶
最終コンテンツをレンダリングするときに使われる
Templateインスタンスのリストで、 レンダリングされる順となります。テンプレートがファイルから読み込まれる場合、テンプレートのファイル名を取得するためには、リスト内の各テンプレートに対してtemplate.nameを使ってください (名前は'admin/index.html'のような文字列となります)。Django のテンプレートを使っていない?
この属性は、
DjangoTemplatesバックエンドを使用しているときのみ格納されます。他のテンプレートを知使っている場合、template_nameが、レンダリングに使われるテンプレートの名前のみが必要な場合の適切な代替策となるでしょう。
- resolver_match¶
レスポンスに対する
ResolverMatchのインスタンスです。たとえば、次のようにレスポンスを提供するビューを検証するためにfunc属性を使えます。# my_view here is a function based view. self.assertEqual(response.resolver_match.func, my_view) # Class-based views need to compare the view_class, as the # functions generated by as_view() won't be equal. self.assertIs(response.resolver_match.func.view_class, MyView)
指定された URL が見つからない場合、この属性にアクセスすると
Resolver404例外が投げられます。
通常のレスポンスと同様に、HttpResponse.headers 経由でヘッダにアクセスできます。たとえば、レスポンスの content type は、response.headers['Content-Type'] を使って決定できます。
例外¶
例外を発生させるビューをテストクライアントで指定し、Client.raise_request_exception が True である場合、例外はテストケース内で可視化されます。その後、標準の try ... except ブロックを使うか、assertRaises() で例外をテストできます。
テストクライアントに可視化されない唯一の例外は、Http404、PermissionDenied、SystemExit、SuspiciousOperation です。Django はこれらの例外を内部でキャッチし、適切な HTTP レスポンスコードに変換します。これらの場合、テストでは response.status_code をチェックできます。
Client.raise_request_exception が False の場合、テストクライアントはブラウザに返されることになる 500 レスポンスを返します。レスポンスには exc_info 属性があり、未処理の例外に関する情報が提供されます。
永続的なステート¶
テストクライアントはステートフルです。レスポンスがクッキーを返す場合、そのクッキーはテストクライアント内に保持され、以降すべての get() および post() リクエストともに送信されます。
クッキーに対する有効期限ポリシーは使えません。クッキーを期限切れにしたい場合、手動で削除するか、新たに (すべてのクッキーを削除するために) Client を作成してください。
テストクライアントは、永続的なステート情報を保持する属性を持ちます。テスト条件の一環として、これらのプロパティにアクセスできます。
- Client.cookies¶
Python の
SimpleCookieオブジェクトで、すべてのクライアントのクッキーの現在の値を含みます。より詳しくは、http.cookiesのドキュメントを参照してください。
- Client.session¶
辞書形式のようなオブジェクトで、セッション情報を含みます。セッションのドキュメント に網羅的情報があります。
セッションを修正し保存するためには、まず変数に格納される必要があります (新しい
SessionStoreは、この属性がアクセスされるたびに作られるためです):def test_something(self): session = self.client.session session["somekey"] = "test" session.save()
言語の設定¶
国際化とローカライズをサポートしているアプリケーションをテストする際、テストクライアントのリクエストに対して言語を設定するのがよいでしょう。その方法は、LocaleMiddleware が有効化されているか否かによって異なります。
ミドルウェアが有効化されている場合、LANGUAGE_COOKIE_NAME の名前でクッキーを作成することでセットすることができ、言語コードの値は次のようになります。
from django.conf import settings
def test_language_using_cookie(self):
self.client.cookies.load({settings.LANGUAGE_COOKIE_NAME: "fr"})
response = self.client.get("/")
self.assertEqual(response.content, b"Bienvenue sur mon site.")
もしくは、Accept-Language HTTP ヘッダをリクエスト内に含めます。
def test_language_using_header(self):
response = self.client.get("/", headers={"accept-language": "fr"})
self.assertEqual(response.content, b"Bienvenue sur mon site.")
注釈
これらのメソッドを使用するときは、必ず各テストの最後にアクティブな言語をリセットしてください。
def tearDown(self):
translation.activate(settings.LANGUAGE_CODE)
さらなる詳細は Django はどうやって言語の優先順位を検出するのか? にあります。
ミドルウェアが有効化されていない場合、アクティブな言語は translation.override() を使って設定できます。
from django.utils import translation
def test_language_using_override(self):
with translation.override("fr"):
response = self.client.get("/")
self.assertEqual(response.content, b"Bienvenue sur mon site.")
さらなる詳細は 明示的にアクティブな言語をセットする にあります。
カスタマイズ例¶
以下は、テストクライアントを使ったユニットテストです。
import unittest
from django.test import Client
class SimpleTest(unittest.TestCase):
def setUp(self):
# Every test needs a client.
self.client = Client()
def test_details(self):
# Issue a GET request.
response = self.client.get("/customer/details/")
# Check that the response is 200 OK.
self.assertEqual(response.status_code, 200)
# Check that the rendered context contains 5 customers.
self.assertEqual(len(response.context["customers"]), 5)
提供されるテストケースのクラス¶
標準の Python ユニットテストのクラスは、unittest.TestCase のベースクラスを拡張しています。Django は、このベースクラスのいくつかの拡張を提供します。
Django ユニットテストクラスの階層¶
通常の unittest.TestCase は、任意のサブクラスに変換できます。つまり、テストのベースクラスを unittest.TestCase からサブクラスに変更します。すべての標準の Python のユニットテスト機能が利用可能なまま、以下の各セクションで説明するような便利な追加機能を追加できます。
SimpleTestCase¶
以下の機能を追加する、unittest.TestCase のサブクラスです。
便利なアサーションには以下のようなものがあります。
実行可能な関数が
特定の例外を送出することをチェックする。実行可能な関数が
特定の warning をトリガーすることをチェックする。フォームのフィールドの
レンダリングとエラー処理をテストする。HTML レスポンスに指定されたフラグメントが表示/非表示されることをテストする。テンプレートが
指定されたレスポンスのコンテンツを生成するために使われた/使われなかったことを検証する。2つの
URLが等しいかどうかを検証する。HTTP
リダイレクトがアプリによって実行されたことを検証する。2つの
HTML フラグメントの等価性/非等価性、または包含関係 (containment)をロバストにテストする。2つの
XML フラグメントの等価性/非等価性をロバストにテストする。2つの
JSON フラグメントの等価性をロバストにテストする。
編集した設定 でテストを実行する機能。
データベースクエリを作る手薄との場合、サブクラス TransactionTestCase や TestCase を使用してください。
- SimpleTestCase.databases¶
SimpleTestCaseはデフォルトでデータベースクエリを許可しません。各SimpleTestCaseテストはトランザクション内で実行されないため、これにより、他のテストに影響する書き込みクエリの実行を避けられます。この問題が心配でない場合は、テストクラスのdatabasesクラス属性を'__all__'に設定することで、この振る舞いを無効化できます。
警告
SimpleTestCase とそのサブクラス (例: TestCase など) は、クラス横断的な初期化を実現するために setUpClass() と tearDownClass() に依存しています (例: 設定のオーバーライド)。これらをオーバーライドする必要があるときは、次のように super 実装を呼び出すのを忘れないでください。
class MyTestCase(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
...
@classmethod
def tearDownClass(cls):
...
super().tearDownClass()
setUpClass() で例外が発生した場合の Python の動作を必ず考慮してください。もし例外が起きた場合は、クラス内のテストも tearDownClass() も実行されません。django.test.TestCase の場合、super() 内で作成されたトランザクションがリークし、一部のプラットフォーム (macOS で報告されています) での segmentation fault を含む、さまざまな症状が起きる結果となります。setUpClass() 内での unittest.SkipTest などの例外を意図的に発生させたい場合は、この問題を避けるために、必ず super() を呼び出す前に実行するようにしてください。
TransactionTestCase¶
TransactionTestCase は SimpleTestCase を継承し、以下のようなデータベースに特有の機能を追加しています。
Django の TestCase クラスは、より一般的に使用される TransactionTestCase のサブクラスで、データベース トランザクション機能を活用して、各テストの開始時にデータベースを既知の状態にリセットする処理を高速化します。しかし、この結果として、一部のデータベースの振る舞いを Django の TestCase クラスでテストできなくなります。たとえば、select_for_update() の使用が必要なため、コードブロックをトランザクション内で実行するテストが行えません。そのような場合には、TransactionTestCase を使う必要があります。
TransactionTestCase と TestCase は、以下のように、データベースを既知の状態にリセットする方法と、コミットとロールバックの効果をテストするコードの機能を除いて同一です。
TransactionTestCaseは、テスト実行後にすべてのテーブルを truncate することでデータベースをリセットします。TransactionTestCaseはコミットとロールバックを呼び出して、これらの呼び出しのデータベースへの効果を観察できます。一方、
TestCaseは、テスト後にテーブルを truncate しません。代わりに、テスト終了時にロールバックされるデータベーストランザクション内にテストコードを含めます。これにより、テスト終了時のロールバックによりデータベースが初期状態に復元されることを保証します。
警告
ロールバックをサポートしないデータベース (例: MyISAM ストレージエンジンを使用した MySQL) 上で実行される TestCase と、TransactionTestCase のすべてのインスタンスは、テスト終了時にテストデータベースからすべてのデータを削除することによってロールバックされます。
アプリは データの再読み込みを確認しません。そのため、この機能が必要な場合には (たとえば、サードパーティのアプリはこれを有効化する必要があります)、TestCase 本体内で serialized_rollback = True を設定できます。
TestCase¶
これは、Django でテストを書く際に使われる、最も一般的なクラスです。TransactionTestCase (および、拡張による SimpleTestCase) を継承します。あなたの Django アプリケーションがデータベースを使用しない場合は、SimpleTestCase を使ってください。
クラス:
2 つのネストされた
atomic()ブロックでテストをラップします。1 つはテスト全体、もう 1 つは各テストのためです。したがって、特定のデータベーストランザクションの振る舞いをテストしたい場合は、TransactionTestCaseを使ってください。各テストの最後に、deferrable なデータベース制約をチェックします。
さらに、以下のような追加メソッドを提供します。
- classmethod TestCase.setUpTestData()[ソース]¶
上述のクラスレベルの
atomicブロックは、全体のTestCaseに対して一度、クラスレベルでの初期データの作成を可能にします。 この技法により、setUp()を使うのに比べて高速なテストとなります。例:
from django.test import TestCase class MyTests(TestCase): @classmethod def setUpTestData(cls): # Set up data for the whole TestCase cls.foo = Foo.objects.create(bar="Test") ... def test1(self): # Some test using self.foo ... def test2(self): # Some other test using self.foo ...
テストがトランザクションサポートのないデータベース (たとえば MyISAM エンジンの MySQL) で実行される場合、各テストの前に
setUpTestData()が呼ばれ、高速化のメリットはなくなります。Objects assigned to class attributes in
setUpTestData()内でクラス属性に代入されたオブジェクトは、各テストメソッドで実行される変更から隔離するために、copy.deepcopy()を使用したディープコピーの作成をサポートしなければなりません。
- classmethod TestCase.captureOnCommitCallbacks(using=DEFAULT_DB_ALIAS, execute=False)[ソース]¶
指定されたデータベースコネクションに対する
transaction.on_commit()コールバックをキャプチャしたコンテキストマネージャを返します。コンテキストの終了時に、キャプチャされたコールバック関数を含むリストを返します。このリストから、コールバック上のアサーションを作成したり、その副作用を起こすために呼び出したり、コミットをエミュレートしたりできます。usingは、コールバックをキャプチャするためのデータベースコネクションのエイリアスです。executeがTrueの場合、コンテキストマネージャの終了時に、すべてのコールバックが呼ばれます。これは、ラップされたコードブロック後のコミットをエミュレートします。例:
from django.core import mail from django.test import TestCase class ContactTests(TestCase): def test_post(self): with self.captureOnCommitCallbacks(execute=True) as callbacks: response = self.client.post( "/contact/", {"message": "I like your site"}, ) self.assertEqual(response.status_code, 200) self.assertEqual(len(callbacks), 1) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, "Contact Form") self.assertEqual(mail.outbox[0].body, "I like your site")
LiveServerTestCase¶
LiveServerTestCase は TransactionTestCase とほぼ同じですが、セットアップ時に実際の Django サーバーをバックグラウンドで起動し、終了時に破棄するという追加機能が1つあります。これにより、たとえば Selenium クライアントのような Django ダミークライアント 以外の自動化されたテストクライアントを使えるようになり、ブラウザ内での一連の機能テストを実施して、実際のユーザーの行動をシミュレートできます。
ライブサーバーは localhost をリッスンして、オペレーティングシステムが空きポートに割り当てるために使われるポート 0 にバインドします。サーバーの URL はテスト中は self.live_server_url でアクセスできます。
LiveServerTestCase の使用方法を紹介するために、Selenium テストを書いてみましょう。最初に、 selenium パッケージをインストールする必要があります。
$ python -m pip install "selenium >= 4.8.0"
...\> py -m pip install "selenium >= 4.8.0"
その後、LiveServerTestCase ベースのテストをアプリのテストモジュール (例: myapp/tests.py) に追加します。たとえば、staticfiles アプリを使っていて、開発時に DEBUG=True を使用したときと同じように、つまり、collectstatic を使って静的ファイルを集める必要なく、静的ファイルがテストの実行中に配信されてほしいとします。その機能を提供する StaticLiveServerTestCase サブクラスを使うことになります。それが必要ない場合は、django.test.LiveServerTestCase で置き換えてください。
このテストに対するコードは、以下のようになるでしょう。
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.webdriver import WebDriver
class MySeleniumTests(StaticLiveServerTestCase):
fixtures = ["user-data.json"]
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.selenium = WebDriver()
cls.selenium.implicitly_wait(10)
@classmethod
def tearDownClass(cls):
cls.selenium.quit()
super().tearDownClass()
def test_login(self):
self.selenium.get(f"{self.live_server_url}/login/")
username_input = self.selenium.find_element(By.NAME, "username")
username_input.send_keys("myuser")
password_input = self.selenium.find_element(By.NAME, "password")
password_input.send_keys("secret")
self.selenium.find_element(By.XPATH, '//input[@value="Log in"]').click()
最後に、次のようにテストを実行できます。
$ ./manage.py test myapp.tests.MySeleniumTests.test_login
...\> manage.py test myapp.tests.MySeleniumTests.test_login
この例では、Firefox が自動的に開き、ログインページに行き、クレデンシャルを入力して、「Log in」ボタンをクリックします。Selenium は、Firefox をインストール済みでないか、他のブラウザを使いたいときのために、他のドライバも提供します。上の例は、Selenium クライアントができることのほんの一部に過ぎません。詳細については、完全なリファレンス を確認してください。
注釈
インメモリ SQLite データベースを使用してテストを実行する場合、同じデータベース接続を 2 つのスレッドで並行して共有します。ライブサーバを実行するスレッドとテストケースを実行するスレッドです。この 2 つのスレッドが共有接続を介して同時にデータベースをクエリしないようにすることが重要です。これは、テストがランダムに失敗する可能性があるからです。そのため、2 つのスレッドが同時にデータベースにアクセスしないようにする必要があります。特に、場合によっては (たとえばリンクをクリックしたりフォームを送信したりした直後に) Selenium がレスポンスを受け取り、次のページが読み込まれたことを確認してからテストを実行する必要があるかもしれません。たとえば、レスポンスの中に <body> という HTML タグが見つかるまで Selenium を待たせるには、下記のようにします (Selenium > 2.13 が必要です):
def test_login(self):
from selenium.webdriver.support.wait import WebDriverWait
timeout = 2
...
self.selenium.find_element(By.XPATH, '//input[@value="Log in"]').click()
# Wait until the response is received
WebDriverWait(self.selenium, timeout).until(
lambda driver: driver.find_element(By.TAG_NAME, "body")
)
ここで厄介なのは、特にサーバーが最初のドキュメントを生成した後に動的にHTMLを生成する最近のウェブアプリでは、「ページの読み込み」というものが実際には存在しないということです。そのため、レスポンスに <body> があるかどうかをチェックすることは、必ずしもすべてのユースケースに適しているとは限りません。詳しくは Selenium FAQ と Selenium documentation を参照してください。
テストケースの機能¶
デフォルトのテストクライアント¶
- SimpleTestCase.client¶
django.test.*TestCase インスタンス内のすべてのテストケースは、Django のテストクライアントにアクセスできます。このクライアントは、self.client としてアクセスできます。このクライアントはテストごとに再作成されるため、(クッキーのような) ステートがテスト間で持ち越されることを心配する必要はありません。
つまり、各テスト内で Client をインスタンス化する代わりに、次のようにします。
import unittest
from django.test import Client
class SimpleTest(unittest.TestCase):
def test_details(self):
client = Client()
response = client.get("/customer/details/")
self.assertEqual(response.status_code, 200)
def test_index(self):
client = Client()
response = client.get("/customer/index/")
self.assertEqual(response.status_code, 200)
次のように self.client を参照できます。
from django.test import TestCase
class SimpleTest(TestCase):
def test_details(self):
response = self.client.get("/customer/details/")
self.assertEqual(response.status_code, 200)
def test_index(self):
response = self.client.get("/customer/index/")
self.assertEqual(response.status_code, 200)
テストクライアントをカスタムする¶
- SimpleTestCase.client_class¶
異なる Client クラスを使用したい場合 (たとえば独自の動作を持つサブクラス)、次のように client_class クラス属性を使ってください。
from django.test import Client, TestCase
class MyTestClient(Client):
# Specialized methods for your environment
...
class MyTest(TestCase):
client_class = MyTestClient
def test_my_stuff(self):
# Here self.client is an instance of MyTestClient...
call_some_test_code()
フィクスチャのロード¶
- TransactionTestCase.fixtures¶
データベースを利用したウェブサイト向けのテストケースクラスは、データベースにデータが何もなければあまり役に立ちません。TestCase.setUpTestData() などで ORM を使用してオブジェクトを作成すると、テストがよりリーダブルになってメンテナンス性が高まりますが、フィクスチャ を活用することもできます。
フィクスチャは、Django がどうやってデータベースにインポートするか分かっているデータのコレクションです。たとえば、サイトがユーザーアカウントを持つ場合、テスト内でデータベースに格納させるために、フェイクのユーザーアカウントのフィクスチャをセットアップできます。
フィクスチャを作成するもっとも分かりやすい方法は、manage.py dumpdata コマンドを使うことです。これは、データベース内にすでに何らかのデータがあるという仮定に基づいています。詳細は、dumpdata のドキュメント 参照してください。
いったんフィクスチャを作成して INSTALLED_APPS 内ののどれかで fixtures 辞書に記述したら、django.test.TestCase サブクラスで fixtures クラス属性を指定することで、ユニットテスト内でこのフィクスチャを使えます。
from django.test import TestCase
from myapp.models import Animal
class AnimalTestCase(TestCase):
fixtures = ["mammals.json", "birds"]
def setUp(self):
# Test definitions as before.
call_setup_methods()
def test_fluffy_animals(self):
# A test that uses the fixtures.
call_some_test_code()
具体的には、以下のことが起こります。
各テストの開始時、
setUp()が実行される前に、Django はデータベースを flush し、データベースをmigrateが呼ばれた直後の状態に戻します。そして、すべての名前付きフィクスチャがインストールされます。この例では、Django は
mammalsと名付けられたすべての JSON、birdsと名付けられたすべてのフィクスチャの順にインストールします。フィクスチャの定義とインストールに関するより詳しい情報については、フィクスチャ (fixture) トピックを参照してください。
パフォーマンス上の理由から、TestCase がフィクスチャをロードするのは、各テストの前ではなく setUpTestData() の前に、テストクラス全体で1回だけです。各テスト前にデータベースをクリーンアップするためにはトランザクションを使います。いずれの場合も、テストの結果が、他のテストやテストの実行順序に影響を受けないことは確実です。
デフォルトでは、フィクスチャは default データベースにだけロードされます。複数のデータベースと TransactionTestCase.databases を使用している場合は、フィクスチャは指定されたすべてのデータベースにロードされます。
URLconf の設定¶
アプリケーションがヴューを提供する場合、テストクライアントを使ってビューを実行するテストを含めたくなるかもしれません。しかし、エンドユーザーは、アプリケーション内で選択した任意の URL に自由にビューをデプロイできます。これが意味するのは、テストはビューが特定の URL で利用可能であるという事実に頼ることができないということです。URLconf の設定のためには、テストクラスまたはメソッドを @override_settings(ROOT_URLCONF=...) を使用してデコレーションしてください。
マルチデータベースのサポート¶
- TransactionTestCase.databases¶
Django は、設定の DATABASES で定義され、databases を介して少なくとも1つのテストで参照されるデータベースそれぞれに対応するテストデータベースをセットアップします。
しかし、 Django の TestCase の実行にかかる時間の大部分は、各テストの開始時にデータベースがクリーンであることを保証する flush の呼び出しに消費されます。複数のデータベースがある場合、複数回のフラッシュが必要であり (各データベースに 1 回ずつ)、特に複数データベースの動作をテストする必要がないテストでは、これは時間のかかる作業です。
最適化として、Django は各テスト実行の開始時に default データベースだけをフラッシュします。セットアップに複数のデータベースが含まれていて、全てのデータベースをクリーンにする必要があるテストがある場合、テストスイートで databases 属性を使って、追加のデータベースのフラッシュを要求できます。
例:
class TestMyViews(TransactionTestCase):
databases = {"default", "other"}
def test_index_page_view(self):
call_some_test_code()
このテストケースクラスは test_index_page_view を実行する前に default と other のテストデータベースをフラッシュします。また、'__all__' を使用すると、すべてのテストデータベースをフラッシュするように指定できます。
databases フラグは TransactionTestCase.fixtures がどのデータベースに読み込まれるかを制御します。デフォルトでは、フィクスチャは default データベースにのみ読み込まれます。
databases にないデータベースに対するクエリは、テスト間で状態が漏れるのを防ぐためにアサーションエラーを出します。
- TestCase.databases¶
デフォルトでは、TestCase の実行中にトランザクションにラップされるのは default データベースだけで、他のデータベースにクエリを実行しようとすると、テスト間の状態リークを防ぐためにアサーションエラーが発生します。
デフォルト以外のデータベースに対してトランザクションラッピングを要求するには、テストクラスの databases クラス属性を使用します。
例:
class OtherDBTests(TestCase):
databases = {"other"}
def test_other_db_query(self): ...
このテストは other データベースに対するクエリのみを許可します。 SimpleTestCase.databases や TransactionTestCase.databases と同様に、定数 '__all__' を使用して、すべてのデータベースに対するクエリを許可するように指定できます。
設定のオーバーライド¶
警告
テストの設定値を一時的に変更するには、以下の関数を使います。 django.conf.settings を直接操作しないでください。Django はそのような操作の後、元の値を復元しません。
テストの目的で、設定を一時的に変更し、テストコードを実行した後に元の値に戻すと便利なことがよくあります。このような使い方のために、 Django は settings() という Python の標準コンテキストマネージャ (PEP 343 を参照) を提供しています:
from django.test import TestCase
class LoginTestCase(TestCase):
def test_login(self):
# First check for the default behavior
response = self.client.get("/sekrit/")
self.assertRedirects(response, "/accounts/login/?next=/sekrit/")
# Then override the LOGIN_URL setting
with self.settings(LOGIN_URL="/other/login/"):
response = self.client.get("/sekrit/")
self.assertRedirects(response, "/other/login/?next=/sekrit/")
この例では、 with ブロック内のコードに対して、 LOGIN_URL 設定をオーバーライドし、その後にその値を以前の状態にリセットします。
値のリストを含む設定の再定義は難しいことがあります。実際には、値の追加や削除で十分なことも多いです。Django には設定を簡単に変更するための modify_settings() コンテキストマネージャがあります:
from django.test import TestCase
class MiddlewareTestCase(TestCase):
def test_cache_middleware(self):
with self.modify_settings(
MIDDLEWARE={
"append": "django.middleware.cache.FetchFromCacheMiddleware",
"prepend": "django.middleware.cache.UpdateCacheMiddleware",
"remove": [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
],
}
):
response = self.client.get("/")
# ...
各アクションに対して、値のリストまたは文字列を指定できます。値がすでにリストに存在する場合、 append と prepend は何もしません。同様に、値が存在しない場合、 remove は何もしません。
テストメソッドの設定を上書きしたい場合、Django には override_settings() デコレータがあります (PEP 318 を参照)。次のように使います:
from django.test import TestCase, override_settings
class LoginTestCase(TestCase):
@override_settings(LOGIN_URL="/other/login/")
def test_login(self):
response = self.client.get("/sekrit/")
self.assertRedirects(response, "/other/login/?next=/sekrit/")
このデコレータは TestCase クラスにも適用できます:
from django.test import TestCase, override_settings
@override_settings(LOGIN_URL="/other/login/")
class LoginTestCase(TestCase):
def test_login(self):
response = self.client.get("/sekrit/")
self.assertRedirects(response, "/other/login/?next=/sekrit/")
同様に、Django には modify_settings() デコレータがあります:
from django.test import TestCase, modify_settings
class MiddlewareTestCase(TestCase):
@modify_settings(
MIDDLEWARE={
"append": "django.middleware.cache.FetchFromCacheMiddleware",
"prepend": "django.middleware.cache.UpdateCacheMiddleware",
}
)
def test_cache_middleware(self):
response = self.client.get("/")
# ...
このデコレータはテストケースクラスにも適用できます:
from django.test import TestCase, modify_settings
@modify_settings(
MIDDLEWARE={
"append": "django.middleware.cache.FetchFromCacheMiddleware",
"prepend": "django.middleware.cache.UpdateCacheMiddleware",
}
)
class MiddlewareTestCase(TestCase):
def test_cache_middleware(self):
response = self.client.get("/")
# ...
注釈
クラスが渡されると、これらのデコレータはそのクラスを直接変更して返します。変更したコピーを作成して返すわけではありません。そのため、上記の例を微調整して LoginTestCase や MiddlewareTestCase と異なる名前を返り値に割り当てようとすると、元のテストケースクラスがデコレータの影響を受けていることに驚くかもしれません。あるクラスに対して、 modify_settings() は常に override_settings() の後に適用されます。
警告
設定ファイルには、Django の内部の初期化時にだけ参照されるいくつかの設定が含まれています。設定ファイルを override_settings で変更した場合、 django.conf.settings モジュール経由でアクセスすれば設定は変更されますが、 Django の内部からは別の方法でアクセスはされます。これらの設定に override_settings() や modify_settings() を使っても、実際には期待通りの動作をすることはないでしょう。
DATABASES の設定を変更することはお勧めしません。 CACHES の設定を変更することは可能ですが、 django.contrib.sessions のようなキャッシュを使用する内部機能を使用している場合は少し厄介です。例えば、キャッシュされたセッションを使用し、 CACHES を上書きするテストでは、セッションバックエンドを再初期化する必要があります。
最後に、設定をモジュールレベルの定数としてエイリアスすることは避けてください。なぜなら、override_settings() はそのような値には機能しないからです。それらの値はモジュールが最初にインポートされた時にのみ評価されます。
下記のように、設定が上書きされた後にその設定を削除することで、設定がない状態をシミュレートすることもできます:
@override_settings()
def test_something(self):
del settings.LOGIN_URL
...
設定を上書きする場合、アプリのコードが、キャッシュや設定が変更されても状態を保持するような機能を使用しているケースにも対処してください。Django には django.test.signals.setting_changed シグナルがあり、設定が変更されたときに状態をクリーンアップしたり、リセットしたりするためのコールバックを登録できます。
Django 自身は、このシグナルを使って様々なデータをリセットします:
オーバーライドされた設定 |
データのリセット |
|---|---|
USE_TZ, TIME_ZONE |
データベースのタイムゾーン |
TEMPLATES |
テンプレートエンジン |
FORM_RENDERER |
デフォルトのレンダラー |
SERIALIZATION_MODULES |
シリアライザーのキャッシュ |
LOCALE_PATHS、LANGUAGE_CODE |
デフォルトの翻訳と読み込まれた翻訳 |
STATIC_ROOT, STATIC_URL, STORAGES |
ストレージの設定 |
FORM_RENDERER 設定が変更された際にデフォルトのレンダラーをリセットする機能が追加されました。
アプリの隔離¶
- utils.isolate_apps(*app_labels, attr_name=None, kwarg_name=None)¶
ラップされたコンテキスト内で定義されたモデルを、独自の隔離された
appsレジストリに登録します。この機能は、クラスが後できれいに削除され、名前衝突のリスクがないため、テスト用にモデルクラスを作成するときに便利です。隔離されたレジストリが含む必要があるアプリラベルは、個別の引数として渡す必要があります。
isolate_apps()をデコレータまたはコンテキストマネージャとして使用できます。以下に例を示します。from django.db import models from django.test import SimpleTestCase from django.test.utils import isolate_apps class MyModelTests(SimpleTestCase): @isolate_apps("app_label") def test_model_definition(self): class TestModel(models.Model): pass ...
あるいは、次のようにします。
with isolate_apps("app_label"): class TestModel(models.Model): pass ...
デコレータ形式は、クラスにも適用できます。
2つのオプション引数が指定できます。
attr_name: クラスデコレータとして使用される場合に、隔離されたレジストリに割り当てられる属性。kwarg_name: 関数デコレータとして使用された場合に、隔離されたレジストリに渡されるキーワード引数。
モデル登録を隔離するために使用される一時的な
Appsインスタンスは、クラスデコレータとして使用する際にattr_nameパラメータを使用して属性として取得できます。@isolate_apps("app_label", attr_name="apps") class TestModelDefinition(SimpleTestCase): def test_model_definition(self): class TestModel(models.Model): pass self.assertIs(self.apps.get_model("app_label", "TestModel"), TestModel)
あるいは、
kwarg_nameパラメータを使ってメソッドデコレータとして使用する際に、 テストメソッドの引数として指定することもできます。class TestModelDefinition(SimpleTestCase): @isolate_apps("app_label", kwarg_name="apps") def test_model_definition(self, apps): class TestModel(models.Model): pass self.assertIs(apps.get_model("app_label", "TestModel"), TestModel)
テスト用の送信トレイを空にする¶
Django のカスタムの TestCase クラスを使うと、テストランナーは各テストケースの開始時に、テスト用のメール送信トレイの中身を消去します。
テスト中のメールサービスの詳細については、以下の Email services を参照してください。
アサーション¶
Python の通常の unittest.TestCase クラスが assertTrue() や assertEqual() のようなアサーションメソッドを実装しているように、 Django のカスタム TestCase クラスは、Web アプリケーションのテストに便利なカスタムのアサーションメソッドを多数提供しています。
これらのアサーションメソッドで出力される失敗メッセージは、 msg_prefix 引数でカスタマイズできます。この文字列は、アサーションが生成する失敗メッセージの先頭に付加されます。これにより、テストスイートで発生した失敗の場所や原因を特定しやすくなります。
- SimpleTestCase.assertRaisesMessage(expected_exception, expected_message, callable, *args, **kwargs)[ソース]¶
- SimpleTestCase.assertRaisesMessage(expected_exception, expected_message)
callableの実行がexpected_exceptionを引き起こし、かつその例外のメッセージにexpected_messageが含まれていることをアサートします。他の結果はすべて失敗として報告されます。これはunittest.TestCase.assertRaisesRegex()の簡易版で、expected_messageが正規表現として扱われない点が違います。expected_exceptionとexpected_messageパラメータのみが与えられた場合、テストされるコードを関数としてではなくインラインで記述できるように、コンテキストマネージャを返します。with self.assertRaisesMessage(ValueError, "invalid literal for int()"): int("a")
- SimpleTestCase.assertWarnsMessage(expected_warning, expected_message, callable, *args, **kwargs)[ソース]¶
- SimpleTestCase.assertWarnsMessage(expected_warning, expected_message)
これは
SimpleTestCase.assertRaisesMessage()に似ていますが、assertRaisesRegex()の代わりにassertWarnsRegex()と同様のものです。
- SimpleTestCase.assertFieldOutput(fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value='')[ソース]¶
様々な入力に対してフォームフィールドが正しく動作することをアサートします。
- パラメータ:
fieldclass -- テストされるフィールドのクラス。
valid -- 有効な入力をそれらの期待されるクリーンな値にマッピングする辞書。
invalid -- 無効な入力を一つ以上の発生したエラーメッセージにマッピングする辞書。
field_args -- フィールドをインスタンス化するために渡される引数。
field_kwargs -- フィールドをインスタンス化するために渡されるキーワード引数。
empty_value --
empty_valuesにおける入力の期待されるクリーンな出力。
たとえば、以下のコードは
EmailFieldがa@a.comを有効なメールアドレスとして受け入れるが、aaaを合理的なエラーメッセージと共に拒否することをテストします:self.assertFieldOutput( EmailField, {"a@a.com": "a@a.com"}, {"aaa": ["Enter a valid email address."]} )
- SimpleTestCase.assertFormError(form, field, errors, msg_prefix='')[ソース]¶
フォーム上のフィールドが提供されたエラーのリストを発生させることをアサートします。
formはFormインスタンスです。フォームは バインドされている (bound である) 必要がありますが、必ずしもバリデートされている必要はありません (assertFormError()はフォームに対して自動的にfull_clean()を呼び出します)。fieldはチェックするフォームのフィールド名です。フォームの特定のフィールドに関連付けられていないエラーをチェックするには、field=Noneを使用します。errorsは、フィールドが持つことが期待されるすべてのエラー文字列のリストです。1つのエラーのみが期待される場合は、単一のエラー文字列を渡すこともできます。つまり、errors='error message'はerrors=['error message']と同じです。
- SimpleTestCase.assertFormSetError(formset, form_index, field, errors, msg_prefix='')[ソース]¶
レンダリング時に
formsetが指定されたエラーリストを発生させることをアサートします。formsetはFormSetのインスタンスです。フォームセットはバインドされている (bound である) 必要がありますが、必ずしもバリデートされている必要はありません (assertFormSetError()は自動的にフォームセットのfull_clean()を呼び出します)。form_indexはFormSet内のフォームの番号です (0 から始まります)。フォームセットのフォーム以外のエラー、つまりformset.non_form_errors()を呼び出したときに表示されるエラーをチェックするにはform_index=Noneを使用します。この場合、field=Noneも指定する必要があります。fieldとerrorsはassertFormError()のパラメータと同じ意味です。
- SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False)[ソース]¶
responseが与えられたstatus_codeを生成し、そのcontentにtextが現れることをアサートします。もしcountが指定された場合、textはレスポンス中にcount回出現しなければなりません。textを HTML として扱うにはhtmlをTrueに設定します。レスポンスの内容との比較は、一文字ずつ等しくするのではなく、HTMLのセマンティクスに基づいて行われます。ほとんどの場合、空白文字は無視され、属性の順序は重要ではありません。詳細はassertHTMLEqual()を参照してください。Changed in Django 5.1:以前のバージョンでは、エラーメッセージにレスポンス内容が含まれていませんでした。
- SimpleTestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False)[ソース]¶
レスポンスが指定されたstatus_codeを生成し、そのcontentにtextが含まれて いない ことをアサートします。textを HTML として扱うにはhtmlをTrueに設定します。レスポンスの内容との比較は、一文字ずつ等しくするのではなく、HTMLのセマンティクスに基づいて行われます。ほとんどの場合、空白文字は無視され、属性の順序は重要ではありません。詳細はassertHTMLEqual()を参照してください。Changed in Django 5.1:以前のバージョンでは、エラーメッセージにレスポンス内容が含まれていませんでした。
- SimpleTestCase.assertTemplateUsed(response, template_name, msg_prefix='', count=None)[ソース]¶
指定された名前のテンプレートがレスポンスのレンダリングに使われたことをアサートします。
responseはテストクライアントが返すレスポンスインスタンスでなければなりません。template_nameには'admin/index.html'のような文字列を指定します。count引数はテンプレートがレンダリングされるべき回数を示す整数です。デフォルトはNoneで、テンプレートが1回以上レンダリングされるべきことを意味します。このようにコンテキストマネージャとして使用できます:
with self.assertTemplateUsed("index.html"): render_to_string("index.html") with self.assertTemplateUsed(template_name="index.html"): render_to_string("index.html")
- SimpleTestCase.assertTemplateNotUsed(response, template_name, msg_prefix='')[ソース]¶
指定された名前のテンプレートがレスポンスのレンダリングに使われて いない ことをアサートします。
assertTemplateUsed()と同じようにコンテキストマネージャとして使うことができます。
- SimpleTestCase.assertURLEqual(url1, url2, msg_prefix='')[ソース]¶
同じ名前のパラメータを除いて、クエリ文字列パラメータの順序を無視して、2つのURLが同じであることをアサートします。例えば、
/path/?x=1&y=2は/path/?y=2&x=1と等しいですが、/path/?a=1&a=2は/path/?a=2&a=1と等しくありません。
- SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='', fetch_redirect_response=True)[ソース]¶
レスポンスがstatus_codeのリダイレクトステータスを返し、expected_urlにリダイレクトされ (GETデータも含む)、最終ページをtarget_status_codeで受信したことをアサートします。リクエストが
follow引数を使った場合、expected_urlとtarget_status_codeはリダイレクトチェーンの最終地点の url とステータスコードになります。fetch_redirect_responseがFalseの場合、最終ページは読み込まれません。テストクライアントは外部 URL を取得できないので、expected_urlが Django アプリの一部でない場合、これは特に便利です。スキームは、2つのURL間の比較を行う際に正しく処理されます。リダイレクト先にスキームが指定されていない場合、元のリクエストのスキームが使われます。もし存在すれば、
expected_urlのスキームが比較に使われます。
- SimpleTestCase.assertHTMLEqual(html1, html2, msg=None)[ソース]¶
文字列
html1とhtml2が等しいことをアサートします。比較はHTMLのセマンティクスに基づいて行われます。比較は以下のことが考慮されます:HTMLタグの前後の空白は無視されます。
すべての種類の空白は同等とみなされます。
すべての開いたタグは暗黙的に閉じられます。例えば、周囲のタグが閉じられるかHTMLドキュメントが終了したときなど。
空のタグは自己終了 (self-closing) タグと等価です。
HTML要素の属性の順序は重要ではありません。
引数を持たないブール属性 (例:
checked) は、名前と値が同じ属性と等しいです (例を参照)。同じ文字を参照するテキスト、文字参照、およびエンティティ参照は等価です。
以下の例は有効なテストであり、どれも
AssertionErrorを引き起こしません。self.assertHTMLEqual( "<p>Hello <b>'world'!</p>", """<p> Hello <b>'world'! </b> </p>""", ) self.assertHTMLEqual( '<input type="checkbox" checked="checked" id="id_accept_terms" />', '<input id="id_accept_terms" type="checkbox" checked>', )
html1とhtml2はHTMLを含んでいなければなりません。どちらかが解析できない場合はAssertionErrorが発生します。エラー時の出力は
msg引数でカスタマイズできます。
- SimpleTestCase.assertHTMLNotEqual(html1, html2, msg=None)[ソース]¶
文字列
html1とhtml2が等しく ない ことをアサートします。比較はHTMLのセマンティクスに基づいて行われます。詳細はassertHTMLEqual()を参照してください。html1とhtml2はHTMLを含んでいなければなりません。どちらかが解析できない場合はAssertionErrorが発生します。エラー時の出力は
msg引数でカスタマイズできます。
- SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None)[ソース]¶
文字列
xml1とxml2が等しいことをアサートします。比較はXMLセマンティクスに基づいて行われます。assertHTMLEqual()と同様に、比較は構文解析された内容に基づいて行われるため、構文の違いは考慮されず、意味の違いだけが考慮されます。無効なXMLがパラメータに渡された場合、両方の文字列が同じであっても、常にAssertionErrorが発生します。XML宣言、ドキュメントタイプ、処理命令、コメントは無視されます。比較されるのはルート要素とその子要素だけです。
エラー時の出力は
msg引数でカスタマイズできます。
- SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None)[ソース]¶
文字列
xml1とxml2が等しく ない ことをアサートします。比較はXMLセマンティクスに基づいて行われます。詳細はassertXMLEqual()を参照してください。エラー時の出力は
msg引数でカスタマイズできます。
- SimpleTestCase.assertInHTML(needle, haystack, count=None, msg_prefix='')[ソース]¶
HTML フラグメント
needleがhaystackに含まれていることをアサートします。もし
countという整数の引数が指定された場合、さらにneedleの出現数が厳密に検証されます。ほとんどの場合、空白は無視され、属性の順序は重要ではありません。詳細は
assertHTMLEqual()を参照してください。Changed in Django 5.1:以前のバージョンでは、エラーメッセージに
haystackが含まれていませんでした。
- SimpleTestCase.assertNotInHTML(needle, haystack, msg_prefix='')[ソース]¶
- New in Django 5.1.
HTMLフラグメント
needleがhaystackに含まれて いない ことをアサートします。ほとんどの場合、空白は無視され、属性の順序は重要ではありません。詳細は
assertHTMLEqual()を参照してください。
- SimpleTestCase.assertJSONEqual(raw, expected_data, msg=None)[ソース]¶
JSON フラグメント
rawとexpected_dataが等しいことをアサートします。重要な部分はjsonライブラリに委譲されているため、通常の JSON の重要でない空白のルールが適用されます。エラー時の出力は
msg引数でカスタマイズできます。
- SimpleTestCase.assertJSONNotEqual(raw, expected_data, msg=None)[ソース]¶
JSON フラグメント
rawとexpected_dataが等しく ない ことをアサートします。詳細はassertJSONEqual()を参照してください。エラー時の出力は
msg引数でカスタマイズできます。
- TransactionTestCase.assertQuerySetEqual(qs, values, transform=None, ordered=True, msg=None)[ソース]¶
クエリセット
qsが特定の値のイテラブルvaluesにマッチすることをアサートします。transformが指定された場合、valuesはqsの各メンバにtransformを適用したリストと比較されます。デフォルトでは、比較は順序に依存します。もし
qsが暗黙の順序を提供しない場合、orderedパラメータをFalseに設定することで、比較をcollections.Counter比較に変更できます。順序が未定義の場合 (指定されたqsが順序付けされておらず、複数の順序付けられた値との比較の場合)、ValueErrorが発生します。エラー時の出力は
msg引数でカスタマイズできます。
- TransactionTestCase.assertNumQueries(num, func, *args, **kwargs)[ソース]¶
funcが*argsと**kwargsで呼び出されると、num個のデータベースクエリが実行されることをアサートします。もし
kwargsに"using"キーがあれば、それをクエリの数をチェックするためのデータベースのエイリアスとして使用します。self.assertNumQueries(7, using="non_default_db")
もし
usingパラメータを持つ関数を呼び出したい場合は、呼び出しをlambdaでラップして追加のパラメータを指定できます。self.assertNumQueries(7, lambda: my_function(using=7))
コンテキストマネージャとしても使えます。
with self.assertNumQueries(2): Person.objects.create(name="Aaron") Person.objects.create(name="Daniel")
テストにタグをつける¶
テストにタグをつけることで、特定のサブセットを簡単に実行できるようになります。たとえば、速いテストや遅いテストにラベルをつけることができます。
from django.test import tag
class SampleTestCase(TestCase):
@tag("fast")
def test_fast(self): ...
@tag("slow")
def test_slow(self): ...
@tag("slow", "core")
def test_slow_but_core(self): ...
テストケースクラスにタグを付けることもできます。
@tag("slow", "core")
class SampleTestCase(TestCase): ...
サブクラスは基底クラスからタグを継承し、メソッドはそのクラスからタグを継承します。以下の例を考えてみましょう。
@tag("foo")
class SampleTestCaseChild(SampleTestCase):
@tag("bar")
def test(self): ...
SampleTestCaseChild.test は 'slow', 'core', 'bar', そして 'foo' というラベルが付けられます。
その後、どのテストを実行するか選択できます。例えば、速いテストのみを実行するには、次にようにします。
$ ./manage.py test --tag=fast
...\> manage.py test --tag=fast
または、速いテストとコアなテスト (それが遅いとしても) を実行することもできます。
$ ./manage.py test --tag=fast --tag=core
...\> manage.py test --tag=fast --tag=core
タグによってテストを除外することもできます。遅くない場合にコアテストを実行するには、次にようにします。
$ ./manage.py test --tag=core --exclude-tag=slow
...\> manage.py test --tag=core --exclude-tag=slow
test --exclude-tag は test --tag よりも優先されるので、テストに2つのタグがあり、片方を選択してもう片方を除外すると、テストは実行されません。
非同期コードをテストする¶
単に非同期ビューの出力をテストしたいだけであれば、標準のテストクライアントが非同期ループの中で実行するので、追加の作業は必要ありません。
しかし、Django プロジェクトで完全な非同期テストを書きたい場合、いくつかの点を考慮する必要があります。
まず、テストはテストクラスの async def メソッドでなければなりません (非同期コンテキストを与えるため)。Django は自動的に async def テストを検出し、独自のイベントループで実行されるようにラップします。
非同期関数からテストする場合は、非同期テストクライアントも使用する必要があります。これは django.test.AsyncClient として、あるいは任意のテスト上で self.async_client として利用できます。
- class AsyncClient(enforce_csrf_checks=False, raise_request_exception=True, *, headers=None, query_params=None, **defaults)[ソース]¶
AsyncClient は同期 (通常の) テストクライアントと同じメソッドとシグネチャを持ちますが、 以下の例外があります。
初期化では、
defaultsにある任意のキーワード引数が直接 ASGI スコープに追加されます。キーワード引数
extraとして渡されるヘッダには、同期クライアントが必要とするHTTP_プレフィックスを付けてはいけません (Client.get()を参照してください)。例えば、HTTPのAcceptヘッダを設定する方法を示します。>>> c = AsyncClient() >>> c.get("/customers/details/", {"name": "fred", "age": 7}, ACCEPT="application/json")
AsyncClient に follow パラメータが追加されました。
query_params 引数が追加されました。
AsyncClient を使用する場合、リクエストを行うあらゆるメソッドは await にする必要があります。
async def test_my_thing(self):
response = await self.async_client.get("/some-url/")
self.assertEqual(response.status_code, 200)
非同期クライアントは同期ビューを呼び出すこともできます。Django の 非同期のリクエスト経路 を通して実行され、両方に対応しています。AsyncClient を通して呼び出されたビューは、通常のクライアントが作成する WSGIRequest ではなく、ASGIRequest オブジェクトを request として取得します。
警告
テストデコレータを使う場合、正しく動作するためには非同期対応でなければなりません。Django のビルトインデコレータは正しく動作しますが、サードパーティ製のデコレータは実行されないように見えるかもしれません (実行フローの間違った部分を「ラップ」してしまい、あなたのテストは実行されません)。
これらのデコレータを使用する必要がある場合は、テストメソッドをそれらの 内部で async_to_sync() でデコレートするべきです。
from asgiref.sync import async_to_sync
from django.test import TestCase
class MyTests(TestCase):
@mock.patch(...)
@async_to_sync
async def test_my_thing(self): ...
メールサービス¶
Django のビューのどれかが Django のメール機能 を使ってメールを送信している場合、そのビューを使ってテストを実行するたびにメールを送信したくないでしょう。このため、Django のテストランナーは、Django が送信したメールを自動的にダミーの送信トレイにリダイレクトします。これにより、実際にメールを送信することなく、メール送信のあらゆる側面 (メッセージの送信数から各メッセージの内容まで) をテストできます。
テストランナーは、通常のメールバックエンドをテスト用バックエンドに透過的に置き換えることでこれを実現します。(心配しないでください。これは Django 以外のメール送信者、例えばあなたのマシンのメールサーバを動かしているのであれば、それには影響しません。)
- django.core.mail.outbox¶
テスト実行中、各送信メールは django.core.mail.outbox に保存されます。これは送信された全ての EmailMessage インスタンスのリストです。outbox 属性は、locmem メールバックエンドを使用した場合に のみ 作成される特別な属性です。通常は django.core.mail モジュールの一部として存在しないので、直接インポートすることはできません。以下のコードでこの属性に正しくアクセスする方法を示しています。
以下は django.core.mail.outbox の長さと内容を調べるテスト例です。
from django.core import mail
from django.test import TestCase
class EmailTest(TestCase):
def test_send_email(self):
# Send message.
mail.send_mail(
"Subject here",
"Here is the message.",
"from@example.com",
["to@example.com"],
fail_silently=False,
)
# Test that one message has been sent.
self.assertEqual(len(mail.outbox), 1)
# Verify that the subject of the first message is correct.
self.assertEqual(mail.outbox[0].subject, "Subject here")
前述の ように、テストの送信トレイは Django *TestCase の各テストの開始時に空になります。手動で送信トレイを空にするには、mail.outbox に空のリストを代入してください。
from django.core import mail
# Empty the test outbox
mail.outbox = []
管理コマンド¶
管理コマンドは、call_command() 関数を使用してテストできます。以下のように、出力は StringIO インスタンスにリダイレクトできます。
from io import StringIO
from django.core.management import call_command
from django.test import TestCase
class ClosepollTest(TestCase):
def test_command_output(self):
out = StringIO()
call_command("closepoll", poll_ids=[1], stdout=out)
self.assertIn('Successfully closed poll "1"', out.getvalue())
テストのスキップ¶
unittest ライブラリは @skipIf デコレータと @skipUnless デコレータを提供しており、特定の条件下でテストが失敗することがわかっている場合にテストをスキップできます。
例えば、テストを成功させるために特定のオプションライブラリが必要な場合に、テストケースを @skipIf でデコレートすることができます。そうすると、テストランナーはテストを失敗させたり完全に省略したりする代わりに、テストが実行されなかったこととその理由を報告します。
これらのテストスキップ動作を補うために、 Django はさらに 2 つのスキップデコレータを提供しています。一般的な真偽値をテストする代わりに、これらのデコレータはデータベースの機能をチェックし、データベースが特定の機能をサポートしていなければテストをスキップします。
デコレータは、データベース機能を表す文字列識別子を使用します。この文字列はデータベース接続の features クラスの属性に対応します。 django.db.backends.base.features.BaseDatabaseFeatures クラス に、テストをスキップするベースとして使用できるデータベース機能の完全なリストがあります。
指定したデータベース機能がすべてサポートされている場合は、デコレートされたテストや TestCase をスキップします。
例えば、データベースがトランザクションをサポートしている場合、以下のテストは実行されません (例えば、PostgreSQLでは実行 されません が、MyISAMテーブルを持つMySQLでは実行されます)。
class MyTests(TestCase):
@skipIfDBFeature("supports_transactions")
def test_transaction_behavior(self):
# ... conditional test code
pass
指定したデータベース機能がサポートされていない場合は、デコレートされたテストや TestCase をスキップします。
例えば、以下のテストはデータベースがトランザクションをサポートしている場合だけ実行されます (たとえば、PostgreSQLでは実行されますが、MyISAMテーブルを持つMySQLでは実行 されません)。
class MyTests(TestCase):
@skipUnlessDBFeature("supports_transactions")
def test_transaction_behavior(self):
# ... conditional test code
pass