こんにちは。KOUKIです。
前回の続きです。本日は、Blog APIを作成します。
<目次>
記事まとめ
Blog Modelの作成とテスト
Blog Modelを作成します。
まずは、テストコードを書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# app/blog/tests/test_models.py from django.test import TestCase from django.contrib.auth import get_user_model from blog.models import Tag, Comment, Blog # new def sample_user(email='test@selfnote.work', password='SELF NOTE'): return get_user_model().objects.create_user(email, password) class ModelTests(TestCase): def test_tag_str(self): ... def test_comment_str(self): ... def test_blog_str(self): """Check if the blog string representation""" blog = Blog.objects.create( user=sample_user(), title='First Blog Post', body='This is a blog' ) self.assertEqual(str(blog), blog.title) |
Blogが作成されたとき、Titleが文字列として取得できるかについてテストします。
テストを実行します。
1 2 3 4 |
make test app=blog E ImportError: cannot import name 'Blog' from 'blog.models' (/app/blog/models.py) |
Blog Modelがないため、エラーになりました。
Blog Modelを作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# app/blog/models.py from django.db import models from django.conf import settings def set_user(): return models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, ) class Tag(models.Model): name = models.CharField(max_length=255) user = set_user() def __str__(self): return self.name class Comment(models.Model): comment = models.TextField() # fix user = set_user() def __str__(self): return self.comment # new class Blog(models.Model): user = set_user() title = models.CharField(max_length=255) body = models.TextField() link = models.CharField(max_length=255, blank=True) comments = models.ManyToManyField('Comment') tags = models.ManyToManyField('Tag') def __str__(self): return self.title |
Blogモデルを追加しましたので、マイグレーションを実行しましょう。
1 |
make migrate |
テストを実行します。
1 2 3 |
make test app=blog == 13 passed in 2.86s == |
テストに成功しましたので、管理画面からBlogを追加できるようにadmin.pyを修正します。
1 2 3 4 5 6 7 8 9 |
# app/blog/admin.py from django.contrib import admin from .models import Tag, Comment, Blog admin.site.register(Tag) admin.site.register(Comment) admin.site.register(Blog) |
Blog Modelテスト
Blog Modelテストを追加します。
1 |
touch app/blog/tests/test_blog_api.py |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# app/blog/tests/test_blog_api.py from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient from blog.models import Blog from blog.serializers import BlogSerializer BLOGS_URL = reverse('blog:blog-list') def sample_blog(user, **params): defaults = { 'title': 'Sample blog', 'body': 'This is a sample blog', } defaults.update(params) return Blog.objects.create(user=user, **defaults) class PublicBlogApiTests(TestCase): def setUp(self): self.client = APIClient() # 1 def test_auth_required(self): """Check if authentication is required""" res = self.client.get(BLOGS_URL) self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) class PrivateBlogApiTests(TestCase): """Test unauthenticated blog API accesses""" def setUp(self): self.client = APIClient() self.user = get_user_model().objects.create_user( 'test@selfnote.work', 'SELF NOTE' ) self.client.force_authenticate(self.user) # 2 def test_retrieve_blogs(self): """Test retrieving a list of blogs""" sample_blog(user=self.user) sample_blog(user=self.user) res = self.client.get(BLOGS_URL) blogs = Blog.objects.all().order_by('-id') serializer = BlogSerializer(blogs, many=True) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertEqual(res.data, serializer.data) # 3 def test_blogs_limited_to_user(self): """Test retrieving blogs for user""" user2 = get_user_model().objects.create_user( 'other@selfnote.work', 'SELF NOTE' ) sample_blog(user=user2) sample_blog(user=self.user) res = self.client.get(BLOGS_URL) blogs = Blog.objects.filter(user=self.user) serializer = BlogSerializer(blogs, many=True) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertEqual(len(res.data), 1) self.assertEqual(res.data, serializer.data) |
テストの意味については次の通りです。
- #1 -> 未ログインユーザーがアクセスした場合、401エラーを返す
- #2 -> ログイン済みのユーザーがアクセスした場合、Blogリストを返す
- #3 -> ログイン済みのユーザーに関連するBlog情報を返す
テストを実行しましょう。
1 2 3 |
make test app=blog E ImportError: cannot import name 'BlogSerializer' from 'blog.serializers' (/app/blog/serializers.py) |
BlogSerializerが未定義のため、エラーになりました。
BlogSerializerを定義してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# app/blog/serializers.py from rest_framework import serializers from blog.models import Tag, Comment, Blog #new class TagSerializer(serializers.ModelSerializer): class Meta: model = Tag fields = ('id', 'name') read_only_fields = ('id',) class CommentSerializer(serializers.ModelSerializer): class Meta: model = Comment fields = ('id', 'comment') read_only_fields = ('id',) # new class BlogSerializer(serializers.ModelSerializer): comments = serializers.PrimaryKeyRelatedField( many=True, queryset=Comment.objects.all() ) tags = serializers.PrimaryKeyRelatedField( many=True, queryset=Tag.objects.all() ) class Meta: model = Blog fields = ('id', 'title', 'body', 'comments', 'tags', 'link') read_only_fields = ('id',) |
テストを実行します。
1 2 3 |
make test app=blog E django.urls.exceptions.NoReverseMatch: Reverse for 'blog-list' not found. 'blog-list' is not a valid view function or pattern name. |
「BLOGS_URL = reverse(‘blog:blog-list’)」でエラーが出てますね。
urls.pyにパスを追加しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from django.urls import path, include from rest_framework.routers import DefaultRouter from blog import views # https://www.django-rest-framework.org/api-guide/routers/#api-guide router = DefaultRouter() router.register('tags', views.TagViewSet) router.register('comments', views.CommentViewSet) router.register('blogs', views.BlogViewSet) # new app_name = 'blog' urlpatterns = [ path('', include(router.urls)), ] |
テストを実行します。
1 2 3 |
make test app=blog E AttributeError: module 'blog.views' has no attribute 'BlogViewSet' |
BlogViewSetを設定していないので、エラーがでました。
blogアプリのviews.pyに以下を追加しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# app/blog/views.py from rest_framework import viewsets, mixins from rest_framework.authentication import TokenAuthentication from rest_framework.permissions import IsAuthenticated from blog.models import Tag, Comment, Blog # new from .serializers import TagSerializer, CommentSerializer, BlogSerializer # new class BaseBlogAttrViewSet( viewsets.GenericViewSet, mixins.ListModelMixin, mixins.CreateModelMixin): authentication_classes = (TokenAuthentication,) permission_classes = (IsAuthenticated,) def perform_create(self, serializer): serializer.save(user=self.request.user) class TagViewSet(BaseBlogAttrViewSet): queryset = Tag.objects.all() serializer_class = TagSerializer def get_queryset(self): return self.queryset.filter(user=self.request.user).order_by('-name') class CommentViewSet(BaseBlogAttrViewSet): queryset = Comment.objects.all() serializer_class = CommentSerializer def get_queryset(self): return self.queryset.filter(user=self.request.user).order_by('-comment') # new class BlogViewSet(viewsets.ModelViewSet):"" serializer_class = BlogSerializer queryset = Blog.objects.all() authentication_classes = (TokenAuthentication,) permission_classes = (IsAuthenticated,) def get_queryset(self): return self.queryset.filter(user=self.request.user) |
テストを実行します。
1 2 3 |
make test app=blog == 16 passed in 3.16s == |
Blog詳細情報の取得
続いて、Blogの詳細情報を取得する処理を実装します。
最初は、テストコードからです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# app/blog/tests/test_blog_api.py from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient from blog.models import Blog, Comment, Tag # new from blog.serializers import BlogSerializer, BlogDetailSerializer # new BLOGS_URL = reverse('blog:blog-list') # new def detail_url(blog_id): return reverse('blog:blog-detail', args=[blog_id]) # new def sample_tag(user, name='fiction'): return Tag.objects.create(user=user, name=name) # new def sample_comment(user, comment='This is just a comment'): return Comment.objects.create(user=user, comment=comment) def sample_blog(user, **params): defaults = { 'title': 'Sample blog', 'body': 'This is a sample blog', } defaults.update(params) return Blog.objects.create(user=user, **defaults) class PublicBlogApiTests(TestCase): ... class PrivateBlogApiTests(TestCase): """Test unauthenticated blog API accesses""" def setUp(self): ... def test_retrieve_blogs(self): ... def test_blogs_limited_to_user(self): ... # new def test_view_blog_detail(self): """Check if viewing a blog detail""" blog = sample_blog(user=self.user) blog.tags.add(sample_tag(user=self.user)) blog.comments.add(sample_comment(user=self.user)) url = detail_url(blog.id) res = self.client.get(url) serializer = BlogDetailSerializer(blog) self.assertEqual(res.data, serializer.data) |
Blogの詳細が取得できるかのテストを追加しました。
テストを実行します。
1 2 3 |
make test app=blog E ImportError: cannot import name 'BlogDetailSerializer' from 'blog.serializers' (/app/blog/serializers.py) |
BlogDetailSerializerはまだ設定していないため、エラーになりました。
BlogDetailSerializerをserializers.pyに追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# app/blog/serializers.py from rest_framework import serializers from blog.models import Tag, Comment, Blog class TagSerializer(serializers.ModelSerializer): class Meta: model = Tag fields = ('id', 'name') read_only_fields = ('id',) class CommentSerializer(serializers.ModelSerializer): class Meta: model = Comment fields = ('id', 'comment') read_only_fields = ('id',) class BlogSerializer(serializers.ModelSerializer): comments = serializers.PrimaryKeyRelatedField( many=True, queryset=Comment.objects.all() ) tags = serializers.PrimaryKeyRelatedField( many=True, queryset=Tag.objects.all() ) class Meta: model = Blog fields = ('id', 'title', 'body', 'comments', 'tags', 'link') read_only_fields = ('id',) # new class BlogDetailSerializer(BlogSerializer): comments = CommentSerializer(many=True, read_only=True) tags = TagSerializer(many=True, read_only=True) |
テストを実行します。
1 2 3 4 5 6 |
make test app=blog _ PrivateBlogApiTests.test_view_blog_detail _ serializer = BlogDetailSerializer(blog) > self.assertEqual(res.data, serializer.data) E AssertionError: {'id'[70 chars]s': [1], 'tags': [1], 'link': ''} != {'id'[70 chars]s': [OrderedDict([('id', 1), ('comment', 'This[88 chars]: ''} |
シリアライズしていないので、エラーが発生しているようです。
views.pyのBlogViewSetクラスに次の関数を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# app/blog/views.py from rest_framework import viewsets, mixins from rest_framework.authentication import TokenAuthentication from rest_framework.permissions import IsAuthenticated from blog.models import Tag, Comment, Blog from .serializers import TagSerializer, CommentSerializer, BlogSerializer, BlogDetailSerializer # new ... class BlogViewSet(viewsets.ModelViewSet): serializer_class = BlogSerializer queryset = Blog.objects.all() authentication_classes = (TokenAuthentication,) permission_classes = (IsAuthenticated,) def get_queryset(self): return self.queryset.filter(user=self.request.user) # new def get_serializer_class(self): if self.action == 'retrieve': return BlogDetailSerializer return self.serializer_class |
テストを実行します。
1 2 3 |
make test app=blog == 17 passed in 3.28s == |
Blog作成
次は、Blogの作成です。
テストコードを書きましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# app/blog/tests/test_blog_api.py ... class PrivateBlogApiTests(TestCase): ... def test_create_blog(self): """Check if creating blog""" payload = { 'title': 'Blog Love', 'body': 'I like bolg to write for everyone', } res = self.client.post(BLOGS_URL, payload) self.assertEqual(res.status_code, status.HTTP_201_CREATED) blog = Blog.objects.get(id=res.data['id']) for key in payload.keys(): self.assertEqual(payload[key], getattr(blog, key)) def test_create_blog_with_tags(self): """Check if creating a blog with tags""" tag1 = sample_tag(user=self.user, name='blog') tag2 = sample_tag(user=self.user, name='programming') payload = { 'title': 'I love Blog And Programming!!!', 'tags': [tag1.id, tag2.id], 'body': 'Hoge Hoge Hoge', } res = self.client.post(BLOGS_URL, payload) self.assertEqual(res.status_code, status.HTTP_201_CREATED) blog = Blog.objects.get(id=res.data['id']) tags = blog.tags.all() self.assertEqual(tags.count(), 2) self.assertIn(tag1, tags) self.assertIn(tag2, tags) def test_create_blog_with_comments(self): """Check if creating blog with comments""" comment1 = sample_comment(user=self.user, comment='Programming is so cool!!!') comment2 = sample_comment(user=self.user, comment='Especially, Python is so cool!!!') payload = { 'title': 'Programming is very interesting', 'comments': [comment1.id, comment2.id], 'body': 'I am IT Engineer.' } res = self.client.post(BLOGS_URL, payload) self.assertEqual(res.status_code, status.HTTP_201_CREATED) blog = Blog.objects.get(id=res.data['id']) comments = blog.comments.all() self.assertEqual(comments.count(), 2) self.assertIn(comment1, comments) self.assertIn(comment2, comments) |
「Blog単体およびタグやコメント付きで作成できるか」についてテストします。
テストを実行します。
1 2 3 |
make test app=blog == 3 failed, 17 passed in 5.35s == |
エラーになりましたね。
沢山エラーがでるため、エラー詳細については省略させていただきます。
views.pyのBlogViewSetクラスに次の関数を追加しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# app/blog/views.py class BlogViewSet(viewsets.ModelViewSet): serializer_class = BlogSerializer queryset = Blog.objects.all() authentication_classes = (TokenAuthentication,) permission_classes = (IsAuthenticated,) def get_queryset(self): return self.queryset.filter(user=self.request.user) def get_serializer_class(self): if self.action == 'retrieve': return BlogDetailSerializer return self.serializer_class # new def perform_create(self, serializer): serializer.save(user=self.request.user) |
テストを実行します。
1 2 3 |
make test app=blog == 20 passed in 3.76s == |
OKですね。
Blog更新
続いて、Blog更新処理を実装します。
まずは、テストコードですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# app/blog/tests/test_blog_api.py ... class PrivateBlogApiTests(TestCase): ... def test_partial_update_blog(self): """Check if updating a blog with patch""" blog = sample_blog(user=self.user) blog.tags.add(sample_tag(user=self.user)) new_tag = sample_tag(user=self.user, name='Programming') payload = {'title': 'I like Software Development', 'tags': [new_tag.id]} url = detail_url(blog.id) self.client.patch(url, payload) # https://docs.djangoproject.com/en/2.2/ref/models/instances/#django.db.models.Model.refresh_from_db blog.refresh_from_db() self.assertEqual(blog.title, payload['title']) tags = blog.tags.all() self.assertEqual(len(tags), 1) self.assertIn(new_tag, tags) def test_full_update_blog(self): """Check if updating a blog witih put""" blog = sample_blog(user=self.user) blog.tags.add(sample_tag(user=self.user)) payload = { 'title': 'My hobby is reading book', 'body': 'Harry potter is very interesting!!', } url = detail_url(blog.id) self.client.put(url, payload) blog.refresh_from_db() self.assertEqual(blog.title, payload['title']) self.assertEqual(blog.body, payload['body']) tags = blog.tags.all() self.assertEqual(len(tags), 0) |
HTTPメソッドのpatchとputは、次の特性を持ちます。
- patch -> リソースを部分更新するメソッド
- put -> リソースを更新するメソッド
それぞれのメソッドを用いて、Blog更新テストを行います。
「blog.refresh_from_db()」は、データベース更新用の関数です。patchやputでデータ変更を有効にします。
テストを実行します。
1 2 3 4 |
make test app=blog === 22 passed in 4.13s === |
テストがパスしました。
次回
次回は、Media / Filter APIを作成します。
関連記事
基礎編はこちらです。
コメントを残す
コメントを投稿するにはログインしてください。