[Django]Viewのテストを作成してみる

## はじめに
自分用の個人開発のメモ/備忘録として記録していきます。

## 環境
django5.0
↓ dockerでの開発環境作成はこちら

https://qiita.com/mkthrkw/private/588226d30690c707d2b8

↓ 認証ユーザーモデルはこちらをそのまま利用

https://qiita.com/mkthrkw/private/b2ea8fb8f33a68faab87


## やりたいこと
Viewのテストを一通り書いてみる

## 前回

https://qiita.com/mkthrkw/items/c3fcebcbc17fe761bdd1



## テストについて下調べ
おもに下記のようなことを考えてる必要がある模様。
|No|カテゴリ|内容|
|--|----|----|
|1|アクセス制御テスト|適切なユーザーがリソースにアクセスでき、不適切なユーザーがアクセスできないことを確認。例えば、認証されたユーザーのみが特定のエンドポイントを利用できるか、また特定のロールを持つユーザーのみがリソースを編集または削除できるかなど|
|2|入力検証テスト|APIが受け取る入力（POSTやPUTリクエストで送信されるデータ）が適切に検証されていることを確認。無効なデータが拒否され、適切なエラーメッセージが返されることをテスト|
|3|機能テスト|各APIエンドポイントが期待通りに機能するかを確認。リソースの作成、取得、更新、削除など|
|4|エラーハンドリングテスト|APIがエラーを適切に処理し、適切なHTTPステータスコードとエラーメッセージを返すことを確認。例えば、存在しないリソースへのアクセスや無効なアクションの実行など|
|5|パフォーマンステスト|APIが大量のリクエストや大きなデータセットを効率的に処理できるかを確認。大規模なアプリケーションや高負荷環境では重要|
|6|セキュリティテスト|APIがSQLインジェクション、クロスサイトスクリプティング（XSS）、クロスサイトリクエストフォージェリ（CSRF）などの一般的な脆弱性から保護されていることを確認|




## 準備

### とりあえずViewを準備

・user　に紐づいた project に対するViewSetを作成
・userはカスタムユーザーを利用

https://qiita.com/mkthrkw/items/b2ea8fb8f33a68faab87

``` python:views.py
from rest_framework import viewsets
from .models import Project
from .serializers import ProjectSerializer

class ProjectViewSet(viewsets.ModelViewSet):
    queryset = Project.objects.all()
    serializer_class = ProjectSerializer

    def get_queryset(self):
        return Project.objects.filter(user=self.request.user)

    def get_serializer_class(self):
        if self.action == 'retrieve':
            return ProjectDetailSerializer
        return ProjectSerializer
```


## test_views.pyを書いていく

ModelViewSetを使っているため、基本的にはテストは省略してもよいようなのですが、慣れるためにテストを作成する。


### APITestCaseを継承
DRFテストは Django の TestCase を拡張した　APITestCase を利用して行う。APIClient を使用して HTTP リクエストをシミュレートし、レスポンスを評価できる。

``` python: test_views.py
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth import get_user_model
from tms.models import Project, List, Ticket, Comment

test_email = 'testuser@test.com'
test_password = 'testpass'
User = get_user_model()

class ProjectViewSetTestCase(APITestCase):
```

テストの前処理を```setUp()```に記載
認証は```simplejwt```を利用しているので、トークンを取得する処理を追加。

https://qiita.com/mkthrkw/items/57a4296917a54ca34c12

``` python: test_views.py
    def setUp(self):
        self.user = User.objects.create_user(email=test_email, password=test_password)
        response = self.client.post(reverse('token_obtain_pair'), {'email': test_email, 'password': test_password})
        self.token = response.data['access']
        self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.token}')
        self.project = Project.objects.create(name="Test Project", user=self.user)
        self.url = reverse('project-list')
```

### listのテスト
・ステータスと、データ数の確認
``` python: test_views.py
def test_project_list(self):
    response = self.client.get(self.url)
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    self.assertEqual(len(response.data), 1)
```

### createのテスト
・ステータスと、データ数の確認
・name/descriptionの一致確認
``` python: test_views.py
def test_project_create(self):
    response = self.client.post(self.url, {'name': 'New Project', 'description': 'New Description'})
    self.assertEqual(response.status_code, status.HTTP_201_CREATED)
    self.assertEqual(Project.objects.count(), 2)
    self.assertEqual(Project.objects.last().name, 'New Project')
    self.assertEqual(Project.objects.last().description, 'New Description')
```

### retrieveのテスト
・ステータス、データ数の確認
・user > project > list > ticket とリレーションしているデータをそれぞれテスト
``` python: test_views.py
def test_project_retrieve(self):
    self.list = List.objects.create(title="Test List", project=self.project)
    self.ticket = Ticket.objects.create(title="Test Ticket", list=self.list)
    url = reverse('project-detail', args=[self.project.id])
    response = self.client.get(url)
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    self.assertEqual(response.data['name'], 'Test Project')
    self.assertEqual(len(response.data['lists']), 1)
    self.assertEqual(response.data['lists'][0]['title'], 'Test List')
    self.assertEqual(len(response.data['lists'][0]['tickets']), 1)
    self.assertEqual(response.data['lists'][0]['tickets'][0]['title'], 'Test Ticket')
```

### updateのテスト
・ステータスと、更新後の値の一致確認
``` python: test_views.py
def test_project_update(self):
    url = reverse('project-detail', args=[self.project.id])
    response = self.client.patch(url, {'name': 'Updated Name'})
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    response = self.client.get(url)
    self.assertEqual(response.data['name'], 'Updated Name')
```

### deleteのテスト
・ステータス確認
``` python: test_views.py
def test_project_delete(self):
    url = reverse('project-detail', args=[self.project.id])
    response = self.client.delete(url)
    self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
```

### unauthrizedのテスト
・setUpで作成したユーザーとは別のユーザーを作成
・作成したユーザー以外は取得・更新・削除など出来ないことを確認
（querysetでfilterをかけているため404になる）
``` python: test_views.py
def test_project_unauthrized(self):
    self.user = User.objects.create_user(email='unauthrized@test.com', password='unauthrized')
    response = self.client.post(reverse('token_obtain_pair'), {'email': 'unauthrized@test.com', 'password': 'unauthrized'})
    self.token = response.data['access']
    self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.token}')
    url = reverse('project-detail', args=[self.project.id])
    response = self.client.get(url)
    self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
    response = self.client.patch(url, {'name': 'Updated Name'})
    self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
    response = self.client.delete(url)
    self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
```



## test実行
テスト実行
``` shell
python manage.py test
```
全てのテストが成功すればこのような表示となる
``` shell
Found 19 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...................
----------------------------------------------------------------------
Ran 19 tests in 4.555s

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


## おわりに
・仕様通りの返却がされるか、テストを作成するのは非常に重要
・パフォーマンスやセキュリティのテストなど、もっと調べる必要がありそう


## 参考

https://www.django-rest-framework.org/api-guide/testing/#api-test-cases