【Python】【Django】ファイルアップロードのあるフォームとユニットテスト

# 概要
Djangoでファイルアップロードのあるフォームをユニットテストしようとして、解決にやや時間がかかったので忘備録として残しておきます。

## 悩んだところ
 - ファイルインプットの箇所をどうやってテストするか
 - 予め用意したテスト用ファイルでテストするにはどうすれば良いか


# ファイルアップロードのあるフォームについて

 - フォームのフィールドとしては、以下のクラスのフィールドであれば対応したファイルを受け付けるフィールドになる
  - `django.forms.fields.FileField`：ファイル全般
  - `django.forms.fields.ImageField`：画像ファイル
  - 参考：[フォームAPI：Binding uploaded files to a form](https://docs.djangoproject.com/ja/2.2/ref/forms/api/#binding-uploaded-files-to-a-form)
 - テンプレートへの描画については、`widget`キーワードで`django.forms.widgets.FileInput`を指定するとファイルアップロードのinputタグを描画する
 - ファイルアップロードのあるフォームを使う場合は、テンプレートの`form`タグの属性に`enctype="multipart/form-data"`を指定する必要がある
  - 参考：[ファイルのアップロード：ファイルのアップロードの基本](https://docs.djangoproject.com/ja/2.2/topics/http/file-uploads/#basic-file-uploads)



# ユニットテスト

## 結論

- テストするフォームのインスタンス生成時に、以下のものを渡せば良い
 - 第1引数：ファイル以外のフィールドのdict
 - 第2引数：ファイルフィールドのdict
     - テキストファイルのテストならば、dictの値は`django.core.files.uploadedfile.SimpleUploadedFile`を使うと良い
         - 第1引数：ファイル名
         - 第2引数：ファイルの内容（bytes型）
         - 第3引数：コンテンツタイプ（デフォルトでは`text/plain`なので省略可）



## 予め用意したテストファイルでファイルフィールド付きフォームのユニットテストをしたい場合

### テスト対象のフォーム
~~~python:forms.py
from django import forms

class FileUploadForm(forms.Form):
    name = forms.fields.CharField(
        label='名前'
        max_length=50,
        required=True
    )
    file = forms.fields.FileField(
        label='ファイル',
        required=True,
        widget=forms.widgets.FileInput
    )
~~~

### ユニットテスト
 1. テストファイルを`open`メソッドで開き、ファイルハンドルを取得する
  - `with`句を使うと閉じ忘れが防げる
 2. 1.で開いたファイルハンドルの`read`メソッドでファイル内容を読み出す
 3. 2.で読みだしたファイル内容をbytes型に変換する
 4. 3.で変換した内容を`SimpleUploadedFile`のファイル内容として指定する
 5. 4.で生成した`SimpleUploadedFile`のインスタンスをファイルデータのdictの値に設定する
 6. 5.で生成したファイルデータをテスト対象のフォームのコンストラクタの第2引数として渡す

~~~python:tests.py
from django.test.testcases import TestCase
from django.core.files.uploadedfile import SimpleUploadedFile
from .forms import FileUploadForm

class FileUploadFormTest(TestCase):
   def test_fileupload(self):
       # テスト用ファイルを読み込んでアップロードファイルのデータを作成する
       # 同一ディレクトリにテスト用ファイルを配置する前提
       # 1. テストファイルをopenメソッドで開き、ファイルハンドルを取得する
       with open('test.txt') as f:
           # SimpleFileUploadのインスタンスを生成する
           # 2. 開いたファイルのreadメソッドでファイル内容を読み出す
           # 3. 読みだしたファイル内容をbytes型に変換する
           uploadedFile = SimpleUplodedFile(f.name, bytes(f.read(). encording=f.encording))
           # アップロードファイルのデータを作成する
           # 4. SimpleUploadedFileのインスタンスをファイルデータのdictに設定する
           files = {'file': uploadedFile }
       # ファイル以外のフィールドのデータを作成する
       data = {'name': 'ユーザー'}
   # フォームのコンストラクタにテストデータを渡す
   # 第1引数：ファイル以外のフィールドのデータ
   # 第2引数：ファイルフィールドのデータ
   form = FileUploadForm(data, files)
   self.assertTrue(form.is_valid())
~~~



## Djangoでのファイルアップロードについてあれこれ
 - Viewのメソッドが呼び出されたとき、アップロードされたファイルのデータは`request.FILE`に入る
 - Viewのメソッドでリクエストからフォームクラスのインスタンスを生成するとき、コンストラクタへ以下のものを渡すことが出来る
  - 第1引数：`request.POST`（Web画面からPOSTされたテキストやパスワード等のフィールドデータ）
  - 第2引数：`request.FILE`（Web画面からPOSTされたファイルアップロードのデータ）
 - リクエストを受け付けたときのファイルアップロードのデータは、Djangoのデフォルトでは以下のいずれかになっている
  - `django.core.files.uploadedfile.InMemoryUploadedFile`：ファイルサイズが2.5MB未満
  - `django.core.files.uploadedfile.TemporaryUploadedFile`：ファイルサイズが2.5MB以上
  - 参考：[ファイルのアップロード:アップロードハンドラ](https://docs.djangoproject.com/ja/2.2/topics/http/file-uploads/#upload-handlers)