はじめに
自分用の個人開発のメモ/備忘録として記録していきます。
環境
django5.0
↓ dockerでの開発環境作成はこちら
↓ 認証ユーザーモデルはこちらをそのまま利用
やりたいこと
Viewのテストを一通り書いてみる
前回
テストについて下調べ
おもに下記のようなことを考えてる必要がある模様。
No | カテゴリ | 内容 |
---|---|---|
1 | アクセス制御テスト | 適切なユーザーがリソースにアクセスでき、不適切なユーザーがアクセスできないことを確認。例えば、認証されたユーザーのみが特定のエンドポイントを利用できるか、また特定のロールを持つユーザーのみがリソースを編集または削除できるかなど |
2 | 入力検証テスト | APIが受け取る入力(POSTやPUTリクエストで送信されるデータ)が適切に検証されていることを確認。無効なデータが拒否され、適切なエラーメッセージが返されることをテスト |
3 | 機能テスト | 各APIエンドポイントが期待通りに機能するかを確認。リソースの作成、取得、更新、削除など |
4 | エラーハンドリングテスト | APIがエラーを適切に処理し、適切なHTTPステータスコードとエラーメッセージを返すことを確認。例えば、存在しないリソースへのアクセスや無効なアクションの実行など |
5 | パフォーマンステスト | APIが大量のリクエストや大きなデータセットを効率的に処理できるかを確認。大規模なアプリケーションや高負荷環境では重要 |
6 | セキュリティテスト | APIがSQLインジェクション、クロスサイトスクリプティング(XSS)、クロスサイトリクエストフォージェリ(CSRF)などの一般的な脆弱性から保護されていることを確認 |
準備
とりあえずViewを準備
・user に紐づいた project に対するViewSetを作成
・userはカスタムユーザーを利用
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 リクエストをシミュレートし、レスポンスを評価できる。
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
を利用しているので、トークンを取得する処理を追加。
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のテスト
・ステータスと、データ数の確認
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の一致確認
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 とリレーションしているデータをそれぞれテスト
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のテスト
・ステータスと、更新後の値の一致確認
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のテスト
・ステータス確認
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になる)
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実行
テスト実行
python manage.py test
全てのテストが成功すればこのような表示となる
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'...
おわりに
・仕様通りの返却がされるか、テストを作成するのは非常に重要
・パフォーマンスやセキュリティのテストなど、もっと調べる必要がありそう
参考