Djangoのテストをまとめる

# はじめに
現場がユニットテストにフォーカスし始めたので、目検が多かったテスト文化をユニットテストに向けて深化させる（現場はphpだけど）。でもたしかに自動テスト文化になったほうが説得力が違う

# 参考
https://nwpct1.hatenablog.com/entry/how-to-write-unittest-on-django

https://docs.djangoproject.com/en/4.2/topics/testing/overview/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/5caa15b6-ef31-d163-ec6c-309653bc9197.png)

# 大事なこと
- プロダクションコード（実際のコード）がない状態でテストを作る

実際のコードを書く前に絶対に失敗するテストを書く（仮説を立てる）ことが大事

# ディレクトリの状態
アプリケーションは複数ある
- gmarker
- kanban
- linebot
- register
- shopping
- vietnam_research

```console:D/OneDrive/dev/Portfolio
D:.
├─.idea
├─docs
├─favicon
├─import
├─mysite
│  ├─gmarker
│  │  └─tests.py
│  ├─kanban
│  │  └─tests.py
│  ├─linebot
│  │  └─tests.py
│  ├─mysite
│  │  └─tests.py
│  ├─register
│  │  └─tests.py
│  ├─shopping
│  │  └─tests.py
│  │─vietnam_research
│  │  └─tests.py
│  └─manage.py
└─venv
```

# ユニットテストの書き方と実行
`vietnam_research/tests.py` にテストを記述して `python manage.py test` を実行。するとテストは失敗する。

:::note alert
TestCaseの名前空間は `unittest` じゃなくて `django.test`。pycharmからテストできなかったのはこれが原因だった。
```diff_python
- from unittest import TestCase
+ from django.test import TestCase
```
やったー、いつもの三角ボタンからテストできるぞ！:relaxed:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/c73b3566-3c9b-804a-2f20-4ab16c9e76ce.png)
:::
:::note alert
例えば `mixedproject` というリポジトリがあってそのフォルダがルート（`.git` がある）状態になっていて、いろんなスクリプトフォルダがあるなかのひとつに `hogeproject` があってそれがdjangoプロジェクトの起点だった場合、そもそもプロジェクトを分離して `hogeproject` でリポジトリを作るべきである

なぜかテストが実行できなくて地獄を見るぞ。泣く。
自分のプロジェクトだとこんなことしないんだけど、人のリポジトリを間借りしてやってるときなんかはちょっと遠慮しちゃって ｺｺﾆﾂｸﾙﾈ なんてやりはじめるとこんなことになる

import文が `hogeproject` から始まる感じになってたらあやしい
:::
:::note warn
ただ、一応回避方法は発見した。
それは `hogeproject` を起点としてIDEに読み込ませるという方法
:::

```python:tests.py
from django.test import TestCase

class SmokeTest(TestCase):
    def test_bad_maths(self):
        self.assertEqual(1 + 1, 3)  # 失敗
```
## PyCharmのUIでテスト実行
こっちがすき。安定の操作感
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/8ec57459-224d-2173-c5f8-ac24a9e732ee.png)

## コマンドラインからのテスト実行
現場でVSCodeしか使えないような場合の実行の仕方
```console:test実行（配下のアプリケーションすべてのテストを実行してくれるってわけだ）
Portfolio> cd mysite
Portfolio\mysite> python manage.py test
```

## テスト中のデータベースのライフサイクル
さりげなく database を作って、最後に消していることがわかる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/9853717e-53ee-c989-f8ea-7d23586f2289.png)

:::note alert
テストデータベースのライフサイクルはテストコマンドの開始と終了で、 `def` ごとに毎回 `setup()` を実行してくれてそのたびにテーブルはロールバックされるが、pkは増え続けるので `pk=1` みたいなテストを実行すると存在しないと言われてしまう。テーブルから `pk` でひいてはいけない！
:::

settings.py で指定したdatabaseログイン権限に データベース作成 権限がないとだめ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/ef9ff611-cb81-280a-7b95-126033bdd27d.png)

ここまで聞くと簡単そうなんだけど、現場入りするとそもそもテストが走らないみたいな環境要因もあったりするよね。カオス。でも、環境要因が解決してから「どんなテストをすればいいか」というのが思いつかないともっとカオスだからこうして轍（わだち）を残すことが大事。テストっていったらスクリーンショットでしょって文化が僕にもあるけど、そんなものは変えていかないと。。。

# テーブルにコンタクトできるか
### ※migrationsは .gitignore に含めないで！
なんかしらねーけどCIでのテストが通らなくて困っていた![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/63e47f99-d81e-80ce-1dbc-c1ba8045455b.png)

:::note alert
ぼく「もしかしてだけど `migrations` フォルダを `.ignore` すると `python manage.py test` で影響ある？」
gpt「はい、`.gitignore` に `migrations` フォルダを含めていると、`python manage.py test` に影響を与える可能性があります。特に以下のような影響があります」

#### 影響
マイグレーションファイルが存在しない:
`migrations` フォルダ内にはマイグレーションファイル（例えば `0001_initial.py` など）が含まれています。これらのファイルはデータベースのスキーマを管理し、Djangoが適用するマイグレーションの履歴を保持します。もしこのフォルダが `.gitignore` されていて、CI環境などでマイグレーションファイルが存在しない場合、テスト実行時にデータベーススキーマが適用されず、エラーになります。

テスト用データベースにスキーマが反映されない:
テスト用データベースは、マイグレーションを適用することでスキーマが反映されますが、マイグレーションファイルがないと、Djangoは適切にデータベーススキーマを作成できません。その結果、`auth_user` のようなテーブルが存在せず、`django.db.utils.OperationalError` が発生する可能性があります。

#### 解決策
`migrations` フォルダを `.gitignore` から外す: 開発やテスト環境で正しく動作するために、`migrations` フォルダはバージョン管理システムに含めておくことが推奨されます。これにより、`CI/CD` 環境でも正しいマイグレーションが適用され、テスト用データベースが正しく構築されます。

ぼく「ほんとだ　解決したわ。。。」
ぼく「じゃあdjangoの `test` コマンドはマイグレーションファイルを作成せずに既存のマイグレーションファイルを利用するということね？（ローカルでうまくいくのは無視されるファイルが存在するから）」

gpt「はい、その通りです。`python manage.py test` コマンドは、**既存のマイグレーションファイルを利用してテスト環境にデータベースをセットアップします。**」

gpt「つまり、`migrations` フォルダにあるマイグレーションファイルがテストの際に使われるため、もしそのフォルダが `.gitignore` などで無視されていたり、CI環境で存在していないと、マイグレーションが実行されずにエラーが発生する可能性があります。」

gpt「一方で、ローカル環境では `.gitignore` によって無視されるファイルが存在しているので、マイグレーションが正常に動作しているため、問題なくテストが通るということですね。」
:::

:::note info
なんで `migrations` フォルダを `.gitignore` したの？
ぼく「モデルなんてゴロゴロ変わるからさ！」
:::

## No.1 テーブルは0件
```python:Industryモデルはvietnam_researchに実際にあるモデル
from django.test import TestCase
from .models import Industry


class IndustryModelTests(TestCase):
    def test_is_empty(self):
        """ test No.1: テーブルは0件です"""
        saved_industry = Industry.objects.all()
        self.assertEqual(saved_industry.count(), 0)

```

## OK（そらそうよ）
```console:OK
PS D:\OneDrive\dev\Portfolio\mysite> python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.007s

OK
Destroying test database for alias 'default'...
```

## No.2 テーブルにレコードを1件増やす
```python:
from django.test import TestCase
from .models import Industry


class IndustryModelTests(TestCase):
    def test_is_empty(self):
        """ test No.1: テーブルは0件です"""
        saved_industry = Industry.objects.all()
        self.assertEqual(saved_industry.count(), 0)

    def test_is_not_empty(self):
        """test No.2: 1つ登録すれば保存されたレコード数は1"""
        industry = Industry()
        industry.save()
        saved_industries = Industry.objects.all()
        self.assertEqual(saved_industries.count(), 1)
```

## NG（cannot be null）
modelに NOT NULL 制約があるから `pub_date` になんか日付入れないとだめだよ

```console:NG
PS D:\OneDrive\dev\Portfolio\mysite> python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.E
======================================================================
ERROR: test_is_not_empty (vietnam_research.tests.IndustryModelTests)
test No.2: 1つ登録すれば保存されたレコード数は1
----------------------------------------------------------------------
Traceback (most recent call last):
MySQLdb._exceptions.OperationalError: (1048, "Column 'pub_date' cannot be null")

django.db.utils.IntegrityError: (1048, "Column 'pub_date' cannot be null")

----------------------------------------------------------------------
Ran 2 tests in 0.189s

FAILED (errors=1)
Destroying test database for alias 'default'...
```

## テストコード修正
```diff
from django.test import TestCase
+ from django.utils.timezone import now

from .models import Industry


class IndustryModelTests(TestCase):
    def test_is_empty(self):
        """ test No.1: テーブルは0件です"""
        saved_industry = Industry.objects.all()
        self.assertEqual(saved_industry.count(), 0)

    def test_is_not_empty(self):
        """test No.2: 1つ登録すれば保存されたレコード数は1"""
        industry = Industry()
+       industry.pub_date = now()
        industry.save()
        saved_industries = Industry.objects.all()
        self.assertEqual(saved_industries.count(), 1)
```

## OK
たった2文字なのにこの湧き上がる安心感よ。

```console
PS D:\OneDrive\dev\Portfolio\mysite> python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 2.859s

OK
Destroying test database for alias 'default'...
```

## No.3: 入れる前のデータと入れたあとのデータは等しい
```diff
from django.test import TestCase
from django.utils.timezone import now

from .models import Industry


class IndustryModelTests(TestCase):
    def test_is_empty(self):
        """ test No.1: テーブルは0件です"""
        saved_industry = Industry.objects.all()
        self.assertEqual(saved_industry.count(), 0)

    def test_is_not_empty(self):
        """test No.2: 1つ登録すれば保存されたレコード数は1"""
        industry = Industry()
        industry.pub_date = now()
        industry.save()
        saved_industries = Industry.objects.all()
        self.assertEqual(saved_industries.count(), 1)


+   def test_saving_and_get_industry(self):
+       """test No.3: 入れる前のデータと入れたあとのデータは等しい"""
+       first_industry = Industry()
+       market_code, symbol, company_name = "HOSE", "AAA", "アンファット・バイオプラスチック"
+       first_industry.name = market_code
+       first_industry.page = symbol
+       first_industry.publisher = company_name
+       first_industry.save()
+       saved_industries = Industry.objects.all()
+       actual_industry = saved_industries[0]
+       self.assertEqual(actual_industry.market_code, market_code)
+       self.assertEqual(actual_industry.symbol, symbol)
+       self.assertEqual(actual_industry.company_name, company_name)
```

## NG
あれ？予想外のエラーだな。テーブルにデータが入ってない？

```
PS D:\OneDrive\dev\Portfolio\mysite> python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..F
======================================================================
FAIL: test_saving_and_get_industry (vietnam_research.tests.IndustryModelTests)
test No.3: 入れる前のデータと入れたあとのデータは等しい
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\OneDrive\dev\Portfolio\mysite\vietnam_research\tests.py", line 32, in test_saving_and_get_industry
    self.assertEqual(actual_industry.market_code, market_code)
AssertionError: '' != 'HOSE'
+ HOSE

----------------------------------------------------------------------
Ran 3 tests in 0.065s

FAILED (failures=1)
Destroying test database for alias 'default'...
```
とりあえず `print` を入れてみて...

```diff:mysite/vietnam_research/tests.py
from django.test import TestCase
from django.utils.timezone import now

from .models import Industry


class IndustryModelTests(TestCase):
    def test_is_empty(self):
        """ test No.1: テーブルは0件です"""
        saved_industry = Industry.objects.all()
        self.assertEqual(saved_industry.count(), 0)

    def test_is_not_empty(self):
        """test No.2: 1つ登録すれば保存されたレコード数は1"""
        industry = Industry()
        industry.pub_date = now()
        industry.save()
        saved_industries = Industry.objects.all()
        self.assertEqual(saved_industries.count(), 1)

    def test_saving_and_get_industry(self):
        """test No.3: 入れる前のデータと入れたあとのデータは等しい"""
        first_industry = Industry()
        market_code, symbol, company_name = "HOSE", "AAA", "アンファット・バイオプラスチック"
        first_industry.name = market_code
        first_industry.page = symbol
        first_industry.publisher = company_name
        first_industry.pub_date = now()
        first_industry.save()
        saved_industries = Industry.objects.all()
        actual_industry = saved_industries[0]
+       print("actual_industry.market_code: ", actual_industry.market_code)
        self.assertEqual(actual_industry.market_code, market_code)
        self.assertEqual(actual_industry.symbol, symbol)
        self.assertEqual(actual_industry.company_name, company_name)
```

## 検証
```python:mysite/vietnam_research/tests.py
    def test_saving_and_get_industry(self):
        """test No.3: 入れる前のデータと入れたあとのデータは等しい"""
        first_industry = Industry()
        market_code, symbol, company_name = "HOSE", "AAA", "アンファット・バイオプラスチック"
        first_industry.name = market_code
        first_industry.page = symbol
        first_industry.publisher = company_name
        first_industry.pub_date = now()
        first_industry.save()
        saved_industries = Industry.objects.all()
        self.assertEqual(saved_industries.count(), 1)

        # actual_industry = saved_industries[0]
        # self.assertEqual(actual_industry.market_code, market_code)
        # self.assertEqual(actual_industry.symbol, symbol)
        # self.assertEqual(actual_industry.company_name, company_name)
```

この時点ではたしかに入っているな。

```console
PS D:\OneDrive\dev\Portfolio\mysite> python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...
----------------------------------------------------------------------
Ran 3 tests in 0.066s

OK
Destroying test database for alias 'default'...
```

```diff
    def test_saving_and_get_industry(self):
        """test No.3: 入れる前のデータと入れたあとのデータは等しい"""
        first_industry = Industry()
        market_code, symbol, company_name = "HOSE", "AAA", "アンファット・バイオプラスチック"
        first_industry.name = market_code
        first_industry.page = symbol
        first_industry.publisher = company_name
        first_industry.pub_date = now()
        first_industry.save()
        saved_industries = Industry.objects.all()
+       print(Industry.objects.all())
        # actual_industry = saved_industries[0]
        # self.assertEqual(actual_industry.market_code, market_code)
        # self.assertEqual(actual_industry.symbol, symbol)
        # self.assertEqual(actual_industry.company_name, company_name)
```

入ってる... もしかしてもとにした記事が `2014年` だってことが影響してる？？（書き方が違う？自分のソースコード確認するのが一番早いか...ｺﾞｿｺﾞｿ）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/30862bc6-f3c1-6a92-82da-16eb3f9fd93f.png)

```diff:コピペしてたカラムに代入するっちゅー凡ミスやわ...
    def test_saving_and_get_industry(self):
        """test No.3: 入れる前のデータと入れたあとのデータは等しい"""
        first_industry = Industry()
        market_code, symbol, company_name = "HOSE", "AAA", "アンファット・バイオプラスチック"
-       first_industry.name = market_code
+       first_industry.market_code = market_code
-       first_industry.page = symbol
+       first_industry.symbol = symbol
-       first_industry.publisher = company_name
+       first_industry.company_name = company_name
        first_industry.pub_date = now()
        first_industry.save()
        saved_industries = Industry.objects.all()
        actual_industry = saved_industries[0]
        self.assertEqual(actual_industry.market_code, market_code)
        self.assertEqual(actual_industry.symbol, symbol)
        self.assertEqual(actual_industry.company_name, company_name)
```

## OK
いい検証になったわ...

```
PS D:\OneDrive\dev\Portfolio\mysite> python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...
----------------------------------------------------------------------
Ran 3 tests in 0.065s

OK
Destroying test database for alias 'default'...
```

# URL解決のテスト
`vietnam_research` は `index.html` しかないんでね

```python:mysite/vietnam_research/tests.py
class UrlResolveTests(TestCase):
    def test_url_resolves_to_book_list_view(self):
        """test No.4: /では、indexが呼び出される事を検証"""
        found = resolve('/')
        self.assertEqual(found.func, index)
```

## OK
```
PS D:\OneDrive\dev\Portfolio\mysite> python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....
----------------------------------------------------------------------
Ran 4 tests in 0.069s

OK
Destroying test database for alias 'default'...
```

# フォームのテスト（正しいHTMLが返されているか）
```python
    def test_exchange_calc(self):
        """test No.8: 残高4,029,139VND, 単価50,000, 口数200のときに足りない額は7,290,861"""
        update_url = '/'
        # GET the form
        r = self.client.get(update_url)
        # retrieve form data as dict
        form = r.context['exchange_form']
        # manipulate some data
        data = form.initial  # form is unbound but contains data
        data['current_balance'] = 4029139
        data['unit_price'] = 50000
        data['quantity'] = 200
        # POST to the form
        r = self.client.post(update_url, data)
        self.assertContains(r, 'q=-7290861vnd')
```