diadia

興味があることをやってみる。自分のメモを残しておきます。

djnago test ModelFormにForeignKeyが含まれる場合のテスト

例えばItemモデルにCategoryというForeignKeyが含まれているとする。 その上ForeignKeyオブジェクトから選択させる方式でItemオブジェクトを生成している。

Itemオブジェクト生成のためにItemModelFormを作成すると以下のような感じになる。

class ItemModelForm(forms.ModelForm):
    category = forms.ModelChoiceField(queryset=Category.objects.all())

    class Meta:
        model  = Item
        fields = ('category', 'title', 'price','description',  )

このModelFormには一癖ありテストに手こずったのでメモしておく。

ModelChoiceFieldを含むModelFormの挙動について。

ユーザーがModelChoiceFieldから特定のオブジェクトを選択すると、当然ながらModelFormのオブジェクト内にForeignKeyのオブジェクトが格納されてform.is_valid()処理が実行される。 しかしながら手動で同じ環境を作るときは注意が必要であった。ForeignKeyはForeignKeyのオブジェクトのidとしてModelFormに格納されている。したがって以下のようにしてもエラーがずっと出続けるだけで一向に進めないのだ。

category_obj = Category.objects.get(id=2)
data = {
             'category':categoy_obj, 
             'title': "hduhig", 
             'price':100,
             'description':"hsudahisug"
             }

form = ItemModelForm(data)
print(form.is_valid())
>>> False

こうする。 要するにModelFormにForeignKeyを格納する際にはForeignKeyのid値で入れること。

category_obj = Category.objects.get(id=2)
data = {
             'category':categoy_obj.id, 
             'title': "hduhig", 
             'price':100,
             'description':"hsudahisug"
             }

form = ItemModelForm(data)
print(form.is_valid())
>>> True

理由はよくわからないがSerializerのところでもForeignKeyの表示はid値で表現されている。FormのようなタイプにおいてはForeignKeyはid値が扱われる仕様になっているのであろう。

その他djangoのテストに関連するもの

Djangoのテストを実装 - diadia

WebViewの使い方

コンテンツ

  1. ドキュメント
  2. 実装方法
  3. サンプルコード

1.ドキュメント

WebView でのウェブアプリの作成  |  Android デベロッパー  |  Android Developers

2.実装手順

レイアウトでアプリに WebView を追加

<WebView
    android:id="@+id/webview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
/>
val myWebView: WebView = findViewById(R.id.webview)
    myWebView.loadUrl("http://www.example.com")

マニフェストファイルにパーミッションを記述する

<manifest ... >
    <uses-permission android:name="android.permission.INTERNET" />
    ...
</manifest>

基本的な実装はこれで完結する。

JavaScript を有効にする

val myWebView: WebView = findViewById(R.id.webview)
myWebView.settings.javaScriptEnabled = true

3.サンプルコード

GitHub - chiaki1990/WebViewSample

DRFとretrofitの関係(Bodyに関して)

話したいこと

drfのrequest.dataの内容はfieldかbodyかで違いが出る話をしたい。

django rest frameworkを使ってアンドロイドからデータを受け取る場合にはrequest.dataを使う。そしてこのrequest.dataの内容がretrofitのインターフェースメソッドの記述の仕方によって変わることをメモしておく。

retrofitで@Bodyを使った場合にrequest.dataはどのように表示されるか

@POST("api/util/region/")
fun postGetRegionDataByPointAPIView(
    @Header("Authorization") authTokenHeader: String?, 
    @Body wkt_point: String
):Call<ResultRegionModel>

上記のように@Bodyを使ってdjangoに送信した場合には、以下のようにデータを受け取る。

SRID=4326;POINT (-91.51643849909306 14.84320911634316)

retrofitで@Fieldを使った場合にrequest.dataはどのように表示されるか

@FormUrlEncoded
@POST("api/util/region/")
fun postGetRegionDataByPointAPIView(
    @Header("Authorization") authTokenHeader: String?, 
    @Field("wkt_point") wkt_point: String
):Call<ResultRegionModel>

上記のように@Fieldを使ってdjangoに送信した場合のデータは以下である。

<QueryDict: {'wkt_point': ['SRID=4326;POINT (-91.50813136249779 14.84655173537646)']}>

この違いからdjangoで何が変わってくるのか?

送信されたデータの取り出し方が変わってくる。Bodyの場合にはrequest.dataでデータを受け取ることができるが、 Fieldの場合はrequest.dataがdict型なのでrequest.data["wkt_point"]を使ってデータを受けとる事ができる。

Kotlin GoogleMapPlatfotmのtoolbarを削除する

どれ?

f:id:torajirousan:20200529065852p:plain

toolbarの特徴

toolbarはデフォルトで表示され、toolbarをタップすると他のアプリが起動してしまう。 消したい場合はドキュメントに従ってUiSettings.setMapToolbarEnabled(boolean)を実行すれば良い。

サンプルコード

override fun onMapReady(googleMap: GoogleMap?) {

    googleMap!!.uiSettings.isMapToolbarEnabled = false
       
}

toolbarに関するドキュメント

コントロールと操作  |  Maps SDK for Android  |  Google Developers

UiSettings.setMapToolbarEnabledに関するドキュメント

UiSettings  |  Android 用 Google API  |  Google Developers

Django シグナル POST_SAVEのupdate_fieldsをどう使うか

シグナルpost_saveのupdate_fieldsの用途

例えばこんな時に使いたい時にupdate_fieldsが役に立つ。

class Profile(models.Model):
    user = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
    adm0 = models.CharField(max_length=15, default="Japan", choices=adm0_CHOICES)
    adm1  = models.CharField(max_length=15, null=False, choices=DEPARTAMENTO_CHOICES)
    adm2 = models.CharField(max_length=30, null=False, choices=MUNICIPIO_CHOICES)
    description  = models.TextField(default=DEFAULT_PROFILE_DESCRIPTION)
    point = geomodels.PointField(null=True, blank=True)

Ptofileオブジェクトには国の属性を示すadm0、都道府県の属性を示すadm1、市を示すadm2が存在する。一方でユーザーの座標情報を格納するpoint(ジオメトリ型データ)が存在する。

ユーザーのProfileオブジェクトはユーザーの好みによって2種類の生成もしくは変更方法がある。

  • ユーザーが都道府県、市を各リストから選択して登録する方法
  • ユーザーが位置情報を入力し、webアプリが位置情報に合致した都道府県、市を探し登録する方法

後者のProfileオブジェクトのpointが変更されたときだけ、シグナルが発行され、座標データに従って都道府県、市を変更するを実現する場合には、post_saveのcreatedだけではうまく作動させることができない。しかしpost_saveシグナルが発火し、かつpointが変更されたという状況にのみ都道府県、市を変更するコードが動くとしたらやりたいことを実現できる。そしてこれはupdate_fieldsを使うなら実現できる。

イメージサンプルコード

def hoge(sender, instance, created, update_fields, *args, **kwargs):
    if created == True:
        return 
    elif created == False and "point" in  update_fields:
        #point属性値に従ってadm1, adm2を変更するロジックを書く
        return 

post_save.connect(hoge, sender=Profile)

上記のようにコードを記述するだけではシグナルがうまく作動しない。
上記のコードには誤りはないのだけれども、これだけでは足りないのである。

Noneの値が常に出てしまう問題

イメージサンプルコードを走らせるとupdate_fieldsの値がことごとくNoneとなってしまうのである。 したがって期待するロジックが実行されない問題に直面することになる。これはupdate_fieldsの使い方に問題があるからだ。

じゃあupdate_fieldsをどう使うべきなのか?

update_fields(シーケンス型のデータ)はコード記述者が自ら要素を格納しないといけない。 具体的にはあるクラスオブジェクトのsave()メソッド実行時にupdate_fieldsを使うのである。

profile_obj = Profile.objects.get(id=2)
profile_obj.point = point 
profile_pbj.save(update_fields=["point"])

上記のようにオブジェクトの更新に際してupdate_fields=["point"]をsave()メソッドの引数とするのである。
こうすると晴れてシグナルのupdate_fieldsの値はNoneではなく、"point"に関連したデータになるのである。

しかし問題点も存在する。上記の方法ではviews.pyのロジック中に記述しているのだけれども、例えばモバイルアプリケーションを通じてオブジェクトの変更する場合にはロジック中に記述する事ができないのである。 それはモバイルアプリを通じてデータを受信する場合にDRFを使うとSerializerを使ことになるからである。この場合にはserializer.save()を使うのだけれどもこのsave()はupdate_fields=["point"]を許容することができないのだ。 この場合には例えば以下のようにして対応する。

serializer = ProfileSerializer(profile_obj, data=request.data)
if serializer.is_valid():
    if "point" in request.data.keys():
        #シグナルを実行するためにmodel.save()を実行する
        serializer.save()
        serializer.instance.save(update_fields=["point"])
    else:
        serializer.save()

FirebaseCloudMessagingによるプッシュ通知を受信してもアプリを起動できない件

理由がわからない。 サンプルで作ったアプリはプッシュ通知をタップするとアプリが立ち上がったけど、今のアプリは立ち上がらない。

Firebase Notifications は、受信側アプリがフォアグラウンド状態であるかバックグラウンド状態であるかによって、動作が異なります。

フォアグラウンドって具体的にどんな状態を示すのか分かっていない。

フォアグラウンドとバックグラウンドで設定する内容が異なるようだ。
フォアグラウンドの場合には以下が必要。

<service
     android:name=".MyFirebaseMessagingService">
     <intent-filter>
         <action android:name="com.google.firebase.MESSAGING_EVENT"/>
     </intent-filter>
</service>

それでMyFirebaseMessagingServiceのサービスを作成すれば良い。

バックグラウンド用には、

<meta-data
    android:name="com.google.firebase.messaging.default_notification_channel_id"
    android:value="@string/default_notification_channel_id"/>
<!-- Set custom default icon. This is used when no icon is set for incoming notification messages.
     See README(https://goo.gl/l4GJaQ) for more. -->
<meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@drawable/ic_launcher_foreground" />
<!-- Set color used with incoming notification messages. This is used when no color is set for the incoming
     notification message. See README(https://goo.gl/6BKBk7) for more. -->
<meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/colorAccent" />

<string name="default_notification_channel_id" translatable="false">my_app_fcm_default_channel</string>

これらが必要になる。

テスト(ソフトウェアテスト)について

ソフトウェアテストとは、開発者の意図したとおりにソフトウェアが動作するかを検証する行為である。

ソフトウェアテストには種類がある。

テスト名 説明
ユニットテスト メソッド単位のテスト
統合テスト メソッドの組み合わせのテスト
UI/システムテスト データベースやUI部品を組み合わせたテスト

ソフトェアが意図したとおり動作して得られる値を期待値と言い、ソフトウェアが実際に動作して得られた値を実測値という。 そしてこの期待値と実測値の比較検証のことをアサーションと呼ぶ。

ソフトウェアテストを区分するのにブラックボックス/ホワイトボックステストというくくりも存在する。

テスト種類 説明 特徴 / デメリット
ホワイトボックステスト 内部のロジックや仕様を考慮してテストケースを作成する。 ロジックなどを読み取れる必要があるので、ある程度プログラミングの知識が必要 / テストコードが内部構造に強く依存してしまうと、テストコードがプロダクトコードの変更に影響を受けてしまう
ブラックボックステスト ソフトウェアの内部のロジックや仕様を考慮せず、外部から見たときの仕様のみからテストケースを作成する プログラミングの知識よりも業務に関する知識が必要 / 入出力は単純だが内部構造が複雑なものをテストできない

#

ありがちなアンチパターンはすべてのテストを一つのツールでやってしまおうとすること。ツールによって得意不得意があるのでツールの併用を行う方が良いらしい。

スワイプでデータ更新をする

SwipeRefreshLayoutを使えば良い事がわかった。 GlidやLinearの親としてSwipeRefreshLayoutを使う。

参考ドキュメント

スワイプでの更新をアプリに追加する  |  Android デベロッパー  |  Android Developers

参考ドキュメントによるとsupport.v4を使うことになっているが、androidxのバージョンを使いたい。 そのためにimport先を調べておく必要がある。

データの更新が終了したらsetRefreshing(false)をセットする事がわかった。

使い方のイメージをメモ

まずSwipeRefreshLayoutはデフォルトで使えない機能なのでandroidxのモジュールをgradleにインストールする。

//swiperefreshlayoutのインストール : 
//https://developer.android.com/jetpack/androidx/releases/swiperefreshlayout
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'

次にlayoutファイルにandroidx.swiperefreshlayout.widget.SwipeRefreshLayoutビューを加える。このタグ以下にはListViewやRecyclerViewを設置するが、複数のビューを設置する事ができないので気をつけること。

このビューにはsetOnRefreshListener{}がある。スワイプを検知するリスナーで{}内にデータ更新のロジックを記述する。 データ更新が終わったらクルクルを削除するためにswiperefresh.isRefreshing = falseを実行する。 コレで良い。

RecyclerViewでスクロールダウンしてから上に戻ると表示がおかしくなる件

リサイクラービューで上から下にスクロールする時には適切に表示されているのに、 一度下までスクロールして上に戻ると、適切に表示されていたものが変な状態で表示される件に遭遇した。

具体的に自分のケースでは、if文に合致したあるアイテムはグレースケールで画像を表示されるのに下までスクロールして上げてみると別のアイテムがグレースケールの画像になっていた。

コレについてはAdapterに

   override fun getItemId(position: Int): Long {
        //return super.getItemId(position)
        return position.toLong()
    }

    override fun getItemViewType(position: Int): Int {
        //return super.getItemViewType(position)
        return position
    }

コレを追加することで挙動を適正に戻すことができた。理由はドキュメントにあると思うので時間のある時にじっくり調べてみたい。

自分と同じケースに陥ってしまった場合の対処に関する資料

java - RecyclerView messed up data when scrolling - Stack Overflow

Glideの使い方

内容

  1. 普通にGlideを使いたい場合 -> Glideを使う場合の諸設定
  2. GlideAppを使いたい場合 -> GlideAppを使いたい場合の諸設定

Glideを使う場合の諸設定

gradleに以下を設定

dependencies {
  implementation 'com.github.bumptech.glide:glide:4.11.0'
  annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
}

(補足)GlideAppを使いたい場合には、annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'を記述しないでおくこと。

レイアウトファイルにImageViewを追加する。

        <ImageView
        android:id="@+id/show_image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="0"/>   

アクティビティでImageViewオブジェクトを取得し、Glideを使ってImageViewに画像を表示する。

override fun onCreate(savedInstanceState: Bundle?) {

...割愛

// ImageViewオブジェクトを取得する
val iv_show_image = findByIdView&lt;ImageView&gt;(R.Id.show_image)

// 表示したい画像urlをString型データで変数定義する
val urlString = "https://upload.wikimedia.org/wikipedia/commons/thumb/7/74/Kotlin-logo.svg/144px-Kotlin-logo.svg.png"

// Glideライブラリを使用する
Glide.with(activity:this).asBitmap().load(urlString).into(iv_show_image)

}

GlideAppを使いたい場合の諸設定

GlideAppはcircleCrop()等のGlideでは使えない機能を使う場合に使われるモジュールである。 これを使うと画像を単に表示するだけでなく、画像を丸くくり抜いて表示したり編集された状態で画面表示する事ができるようになる。 JavaとKotlinではGlideAppを使う諸設定に違いがあったのでメモしておく。

gradleに以下を設定

dependencies {
    implementation 'com.github.bumptech.glide:glide:4.11.0'
    kapt 'com.github.bumptech.glide:compiler:4.11.0'
}

このままではkaptでエラーが出てしまうので同ファイル(appレベルのgradleファイル)に

apply plugin: 'kotlin-kapt'

を追加する。こんな感じで、

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 29

...省略

次に適当なファイルにAppGlideModuleを拡張したクラスを定義する。 例えばMyAppGlideModule.ktというのを作って、MyAppGlideModuleを定義する。

package com.example.hoge


import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.AppGlideModule

@GlideModule
class MyAppGlideModule : AppGlideModule(){

}

その後にBuildメニューのRebuildを実行する。
するとGlideAppというモジュールを使うことができるようになっている。

GlideApp.with(this).load(imageUrl).circleCrop().into(myImageView);

KotlinでGlideAppを使う場合の手順を説明した資料

Using Glide with Kotlin - Vlonjat Gashi - Medium

Java版GlideAppを使う場合の手順を説明した資料

Android用画像読み込みライブラリ、Glideを使ってみよう! | 株式会社ヌーラボ(Nulab inc.)

KotlinでGlideAppを導入できない問題に出くわした場合の対処に関する資料

cannot resolve symbol 'GlideApp' (GlideApp was not generated) · Issue #1945 · bumptech/glide · GitHub

Glideドキュメント

GitHub - bumptech/glide: An image loading and caching library for Android focused on smooth scrolling

Android端末の通信中にクルクルを表示したい

通信中にクルクルを表示するための機能はProgressDialogとProgressBarがあるようだ。

ProgressDialogはAPI26で非推奨になるらしい。
ProgressBarを使うことになるのか?

2種類あるみたい。 Determinate ProgressBar Indeterminate ProgressBar

ssh接続が遅い場合の対処法

virtual boxにssh接続した結果パスワード入力から認証まで時間がかかり、timeoutになってしまってログインができなかった。(visual stadio code)

この問題に劇的に改善が見られたのはsshd_configファイルのUseDNS=noに変更することだ。

手順を完結に記述しておく。
環境

  • virtual box
  • centOS7

手順

1 接続先のホストにログインする。
2 rootユーザーに変更する。

su root

3 sshd_configの設定項目を変更する

vi /etc/ssh/sshd_config

そして#UseDNS=yesとなっている項目の#を削除し、noに変更する。

#UseDNS=yes
#以下に変更する
UseDNS=no

4 sshの変更を反映させる

systemctl restart sshd
systemctl status sshd

Djangoのテストを実装

分かったこと

  • djangoでテストする項目がわかった。
  • どんな感じでdjangoのテストを書くのか分かった。
  • djangoのテストはどんな種類か

分かったことを軽くまとめる

ふわっと理解する

テストは開発中のアプリが動くことでこうあって欲しい、こうあるべきという結果をまず設定する。 そしてその結果が生じる環境を準備し、その環境下で実際に開発しているアプリを起動させてその結果を得る。 そして最初に設定した結果と実際アプリが動いて得られた結果を照合する。 照らし合わせた結果として実際の結果が想定する結果になれば動作保証がそのテストにおいて保証される。 逆に両者の結果が異なれば、アプリ内のロジックに問題があるか、テスト環境作成に問題があるか、テスト自体に瑕疵があるかである。どれに該当するか考え、該当した箇所を改善し、再びテストを実行する。 djangoにおけるテストを実行するドキュメントは、テストフレームワークを利用した自動化テストに関するものである。 自動化テストとは、テストを起動するコマンドを入力するとこれまで、テストコードが走り、自動的にテストしたい環境を作成し、あるべき結果と現状のアプリが走った結果を照合し、そのテスト結果を記録する。すべてのテストが終了した後にすべてのテスト結果を出力し確認する。

自動化テストのメリットは開発中のアプリの品質を部分的に保証し開発者に安心感を与えること、以前記述したロジックに手を加えることで品質が劣化していないか確認することを手軽にチェックすることができる。

以前持っていた疑問とそれに対する答え

Q1.テストする項目は?

  1. endpointに対して呼び出されるメソッドが正しいか(urls.pyの検証)
  2. endpointに対して使われるテンプレート(html)が正しいか
  3. formに対して与えられるデータによってバリデーションが機能するか(form)
  4. POSTリクエストで投げたデータが正しく保存されているか(views.py)
  5. アクセスするユーザーの種類によってリダイレクトされたり、テンプレートが変わるか(views.py)
  6. 返されるレスポンスのcontextに必要十分のデータが格納されているか(views.py)

Q2. djangoのテストはどんな特徴? ブラックテストとホワイトテストをClientクラスやFactoryクラス?で使い分けることができる。 ただ大体リクエストを投げてレスポンスをテストの対象にすることが多かったのでホワイトテストでテストを書くことが多くなると思う。

基本的にpythonのunuttestを使って実装するイメージをもてばよい。

djangoのテストはほとんどホワイトテストに基づく。つまりdjangoアプリケーションの内部ロジックに基づいたテストを実装する。

Django.test.TestCaseのサブクラスを使ってテストパターンを作成していく。

Djangoにもテストがあるし、GeoDjangoにもテストがあるし、DRFにもテストがある。 

これらをまとめてみたい。

テスト内容とdjangoのモジュールの関係 ユニットテスト

views.pyはclientでテストを実行する。 models.pyもテスト実行する。けどmodelのメソッドに対して実行すると思われる。

test*.pyのファイルに入っているTestCaseを継承したクラスがテストに追加され、実行される。

テストの種類 djangoのunittest
特定の環境下でrequestを投げてどのテンプレートが返されるか、どんなオブジェクトがレンダリングされているかを確認した

seleniumを使ったフロントエンドのテスト
特定の環境下でリクエストを投げて表示されるhtmlのボタンやアンカータグのリンク先が適切なViewのnameが使われているか確認した

django rest frameworkのテスト
特定の環境下でリクエストを投げて返されるオブジェクトを確認した

DBの扱いについて

djangoのテストでは、テスト実行時にテスト用のDBが生成される。そしてテスト終了時にデフォルトでは作成されたDBが削除されることになる。 settings.pyで'TEST'を宣言する必要があると分かった。

DATABASES = {
    'default': {
        'ENGINE': 'django.contrib.gis.db.backends.postgis',
        'NAME': 'postgis_test',
        'PORT': '5432',
        'USER': 'postgisuser',
        'PASSWORD': 'geodjango_passw0rd',
        'TEST':{"NAME" : "test_postgis_db"},
    }
}

テストデータの作成について

テストデータの作成には、実際にendpointを叩いてitemオブジェクトを生成する方法とTestCase#setUpTestDataメソッドを使う方法がある。

実際にエンドポイントを叩く方法は例えばあるメソッド内(setUpまたはあるテスト)でユーザーオブジェクトを生成するエンドポイントと生成するためのユーザーデータをpostする。
こうするとテスト実行時のオブジェクトが生成される。

その他の方法としてオブジェクトをORMを使って生成する方法もある。 例えばItem.objects.create(title="test", price=100)
こんな感じ生成してもテスト実行時のオブジェクト(テスト用のデータ)が生成される。

テストデータの作成はTestCase#setUpを使って生成することを覚えた。 現状としてはこのメソッドでオブジェクトを生成し、各テストでClientオブジェクトを使ってcontextの確認を行うというテストを行っている。

テストデータの作成(Userオブジェクトの作成)

Userオブジェクトの作成は認証ユーザーがrequestを送る場合のテストを検証するため重要になる。 ログインするための使えるUserオブジェクトの作り方は2種類存在する。

  • ユーザー登録を実行するエンドポイントを実装し、そこに作成するUserデータを送信して作成する
  • User.objects.create_user(username="hieh", password="12345")を実行する

誤ってもUser.objects.create()でUserオブジェクトを作成しないこと。作成したとしてもpasswordの再現が困難でありユーザー認証する事ができないから。

Djangoのテストをどう書くか?

例)メソッドに対して返されるHTMLが正しいかどうかを確かめるサンプル

from django.test import TestCase, Client


class HogeTest(TestCase):

    url = "/hoge/idvvokb/"
    def setUp(self):
        User.objects.create_user(
            username="test_user",
            password="12345")


    def test_templates_by_anonymous_user(self):
        self.client = Client()
        login_status = self.client.logout()
        #未認証ユーザーでアクセス
        self.assertFalse(login_status) 


        response = self.client.get(self.url)
        templates = [ele.name for ele in response.templates]
        #hoge/template1.htmlが含まれない
        self.assertTrue("hoge/template1.html" not in templates) 
        #hoge/template2.htmlが含まれない
        self.assertTrue("hoge/template2.html" not in templates) 
        #hoge/template3.htmlが含まれない
        self.assertTrue("hoge/template3.html" not in templates) 
        
    def test_templates_by_authenticated_user(self):
        self.client = Client()
        login_status = self.client.login(username="test_user", password="12345")
        #認証ユーザーでアクセス
        self.assertTrue(login_status) 

        response = self.client.get(self.url)
        templates = [ele.name for ele in response.templates]
        #hoge/template1.htmlが含まれる
        self.assertTrue("hoge/template1.html" in templates)
        #hoge/template2.htmlが含まれる
        self.assertTrue("hoge/template2.html" in templates) 
        #hoge/template3.htmlが含まれる
        self.assertTrue("hoge/template3.html" in templates) 

TestCase#setUpメソッドで行うことはテストデータを作成することだと現状認識している。
setUpメソッドで生成したオブジェクトがdjangoに存在するすべてのオブジェクトになる。(シグナル等の何かをトリガーとして他のオブジェクトが生成される場合はそのオブジェクトもテスト対象のオブジェクトなる。)
Clientクラスはurlに基づいてviewsの各ビューを起動してレスポンスを得ることができる。
htmlファイルが適切に使われるかをチェックするためにはresponseにたいしてtemplatesを使い、その各要素に対してnameを使うと各htmlを参照することができる。

self.clientを各メソッドで初期化しているけれども、これはsetUpでself.clientを初期化しても各メソッドでself.clientの内容が引き継がれてしまうからである。

Formのテスト

送信するデータのバリデーションテスト

import HogeForm

data = {"title": "じじゃおdsv", "price":900 }
form = HogeForm(data)
self.assertTrue(form.is_valid())

レスポンスに使用されるformのクラスが一致しているかのテスト

self.client = Client()
login_status = self.client.login(username="access_user", password="12345")
self.assertTrue(login_status)
response = self.client.get(reverse_lazy('profiles:profile'), follow=True)
self.assertTrue(response.status_code, 200)
self.assertTrue(type(response.context["form"]), ProfileForm)

FormにForeignKeyが含まれる際に注意すること

djnago test ModelFormにForeignKeyが含まれる場合のテスト - diadia

seleniumを使ったテストを実装する

Seleniumを使ったテストのメモ - diadia

テンプレートのテスト

レンダリングされるテンプレートのテスト

django.test.Clientオブジェクトを使ったresponseにはtemplatesがある。 これを使うことでレスポンスで表示されるテンプレートを確認する事ができる。

def test_テンプレートのテスト(self):
    self.client = Client()
    response = self.client.get("/hige/hoge/")
    templates = [template.name for template in response.templates]
    self.assertTrue("hsi/ddjfdi.html" in templates)

またこのようにしなくてもassertTemplateUsedやassertTemplateNotUsedを使ってもテストは行える。

def test_テンプレートのテスト(self):
    self.client = Client()
    response = self.client.get("/hige/hoge/")
    self.assertTemplateUsed(response, template_name="hsi/ddjfdi.html" )
    self.assertTemplateNotUsed(response, template_name="hsi/dcds.html" )

レンダリングされるテンプレートに含まれる要素のテスト

レスポンスとして返されるテンプレートにはhtmlタグやタグの属性値またはタグのテキストが含まれているか確認したい場合は、
self.assert(Not)Contains(response, "文字列") で対応する事ができる。

self.client = Client()
login_status = self.client.login(username="test1", password="1234tweet")
self.assertTrue(login_status)
response = self.client.get(reverse_lazy(ViewName.ITEM_CREATE))
self.assertContains( response, 'action="/items/create2/"', status_code=200 )
self.assertNotContains( response, '/edit/', status_code=200 )

ForeignKeyのフィールドがnullかどうかの判断

if book.author is None:
    print("nullです")

Django Rest Frameworkのテストの作り方

django rest frameworkの場合もホワイトテストでテストを作成する。 その中で困ることがある。それはtokenを使った認可によるリソースのアクセスである。認証の場合には以下のようにすればよかった。

self.client = Client()
login_status = self.client.login(username="hhej", password="saahfa")

認可に関しては当然認証の方法(login)が使えない。したがってrest_frameworkのテストモジュールを利用する必要性がある。

具体的に使うのはAPIClientである。APIClientモジュールにはtokenをヘッダーにセットする機能を有するのでhttpリクエストを実行する前にtokenをセットして使用する。

from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient

token = Token.objects.get(user__username='access_user')
self.client = APIClient()
self.client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)

response = self.client.patch(url)

トークンによるリソースアクセスの方法を示した資料

django rest framework - Add Token to headers request in TEST mode DRF - Stack Overflow

DRFのResponseのデータを参照する方法

Responseの内容はresponse.dataで参照することができる。値はdict型である。

from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient

token = Token.objects.get(user__username='access_user')
self.client = APIClient()
self.client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)

response = self.client.patch(url)
print(response.data)

Django Test Kotlinのretrofitが送信するデータを再現する

retrofitで送信されるデータ形式djangoで再現する。 Django Test retrofitが送信するデータを再現する - diadia

その他注意点

urlは最初に/から始める。/から始めないと404のコードがかえってきてしまうので注意する。
アクセス時にセッションを追加したいときは以下の資料を参照のこと。
https://docs.djangoproject.com/en/dev/topics/testing/tools/#django.test.Client.session

参考資料

Django におけるテスト | Django ドキュメント | Django
Djangoのテストの書き方について勉強したのでまとめる - c-bata web