[Django] 02 - Django REST Framework (DRF)
Ref: https://testdriven.io/courses/tdd-django/django-rest-framework/
DRF is composed of two core components:
- Serializers are used to convert Django model instances to JSON (serialization) and vice versa (deserialization).
- Views (along with ViewSets), which are similar to traditional Django views, handle HTTP requests and return the serialized data. The view itself contains the necessary logic to return the response.
DRF has three types of views:
- Views, which subclasses Django's
View
class, are the most basic (and most explicit) DRF view type. They can be function (implemented via theapi_view
decorator) or class (implemented via theAPIView
class) based. - ViewSets provide a layer of abstraction above DRF views. They are often used to combine the create, read, update, and destroy (CRUD) logic into a single view. A ViewSet "helps ensure that URL conventions will be consistent across your API, minimizes the amount of code you need to write, and allows you to concentrate on the interactions and representations your API provides rather than the specifics of the URL conf". They are perfect for implementing the basic CRUD operations for your API. They can be limiting though if your API goes beyond the basics or the API endpoints don't cleanly map back to the models.
- Generic Views typically take the abstraction further by inferring the response format, allowed methods, and payload shape based on the serializer.
- Views, which subclasses Django's
Ref: Django 3.0 Full Course For Beginners 2020 | Django Step By Step Tutorials【先有了Django的基础,4+hr】
Ref: Django REST Framework Full Course For Beginners | Build REST API With Django【视频教程,重点在本篇过一遍,2+hr】
Ref: Django REST framework 中文教程
Ref: 一图看懂Django和DRF【感觉不错,concise】
Templates 没了;Views 变成了 Views ViewSets Serializers。
开始学习
访问数据库
一、模型 model
调用模型,对数据库进行操作。
装饰器:You can use pytest marks to tell pytest-django
your test needs database access.
# app/tests/movies/test_models.py import pytest from movies.models import Movie @pytest.mark.django_db # 这个是必须要有的 def test_movie_model(): movie = Movie(title="Raising Arizona", genre="comedy", year="1987") movie.save() assert movie.title == "Raising Arizona" assert movie.genre == "comedy" assert movie.year == "1987" assert movie.created_date assert movie.updated_date assert str(movie) == movie.title
model 定义。
# app/movies/models.py from django.db import models from django.contrib.auth.models import AbstractUser class CustomUser(AbstractUser): pass
class Movie(models.Model): title = models.CharField(max_length=255) genre = models.CharField(max_length=255) year = models.CharField(max_length=4) created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) def __str__(self): return f"{self.title}"
数据库操作 物理上实行。然后 pytest。
$ docker-compose exec movies python manage.py migrate
$ docker-compose exec movies python manage.py makemigrations $ docker-compose exec movies pytest
jeffrey@unsw-ThinkPad-T490:django-tdd-docker$ docker-compose exec movies pytest ===================================================================================== test session starts ====================================================================================== platform linux -- Python 3.8.2, pytest-5.4.1, py-1.9.0, pluggy-0.13.1 django: settings: drf_project.settings (from ini) rootdir: /usr/src/app, inifile: pytest.ini plugins: django-3.9.0 collected 3 items tests/movies/test_models.py . [ 33%] tests/test_foo.py .. [100%] ====================================================================================== 3 passed in 0.64s =======================================================================================
二、超级用户 Admin
-
admin page
这么注册了,UI 上才会有对应多出来的表格填写。
# app/movies/admin.py from django.contrib import admin from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin from .models import Movie, CustomUser @admin.register(CustomUser) class UserAdmin(DefaultUserAdmin): pass @admin.register(Movie) class MovieAdmin(admin.ModelAdmin): fields = ( "title", "genre", "year", "created_date", "updated_date", ) list_display = ( "title", "genre", "year", "created_date", "updated_date", ) readonly_fields = ( "created_date", "updated_date", )
-
Create a superuser account
docker-compose exec movies python manage.py createsuperuser
然后,登录并添加若干条数据。
三、Serializers
在 URL --> view --> Serializers中,这里是直接测试 serializer部分链接数据的接口。
-
测试本体
定义一些基本的规则。
# app/movies/serializers.py from rest_framework import serializers from .models import Movie class MovieSerializer(serializers.ModelSerializer): class Meta: model = Movie fields = '__all__' read_only_fields = ('id', 'created_date', 'updated_date',)
-
测试用例
从 serializer 的角度去测试。
# app/tests/movies/test_serializers.py from movies.serializers import MovieSerializer def test_valid_movie_serializer(): valid_serializer_data = { "title": "Raising Arizona", "genre": "comedy", "year": "1987" } serializer = MovieSerializer(data=valid_serializer_data)
assert serializer.is_valid() assert serializer.validated_data == valid_serializer_data assert serializer.data == valid_serializer_data assert serializer.errors == {} def test_invalid_movie_serializer(): invalid_serializer_data = { "title": "Raising Arizona", "genre": "comedy" } serializer = MovieSerializer(data=invalid_serializer_data)
assert not serializer.is_valid() assert serializer.validated_data == {} assert serializer.data == invalid_serializer_data assert serializer.errors == {"year": ["This field is required."]}
-
测试过程
仅测试文件名包含:models 的测试文件。
jeffrey@unsw-ThinkPad-T490:django-tdd-docker$ docker-compose exec movies pytest
============================= test session starts ==============================
platform linux -- Python 3.8.2, pytest-5.4.1, py-1.9.0, pluggy-0.13.1
django: settings: drf_project.settings (from ini)
rootdir: /usr/src/app, inifile: pytest.ini
plugins: django-3.9.0
collected 5 items
tests/movies/test_models.py . [ 20%]
tests/test_foo.py .. [ 60%]
tests/movies/test_serializers.py .. [100%]
============================== 5 passed in 0.73s ===============================
jeffrey@unsw-ThinkPad-T490:django-tdd-docker$ docker-compose exec movies pytest -k models
============================= test session starts ==============================
platform linux -- Python 3.8.2, pytest-5.4.1, py-1.9.0, pluggy-0.13.1
django: settings: drf_project.settings (from ini)
rootdir: /usr/src/app, inifile: pytest.ini
plugins: django-3.9.0
collected 5 items / 4 deselected / 1 selected
tests/movies/test_models.py . [100%]
======================= 1 passed, 4 deselected in 0.73s ========================
RESTful Routes
添加一个 REST API
一、添加新的 url
主urls包含子urls的规则。
# app/drf_project/urls.py from django.contrib import admin from django.urls import include, path from .views import ping urlpatterns = [ path("admin/", admin.site.urls), path("ping/", ping, name="ping"), path("", include("movies.urls")), ]
此处定义好 子urls的规则。
# app/movies/urls.py from django.urls import path from .views import MovieList urlpatterns = [ path("api/movies/", MovieList.as_view()), ]
urls --> views
# app/movies/views.py from django.http import Http404 from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from .models import Movie from .serializers import MovieSerializer class MovieList(APIView): def post(self, request, format=None): serializer = MovieSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
有了 rest_framework,也便有了如下的 UI,带来便利。
二、测试新的 url
两种测试方式,命令行,或者 script mode。
(1) cmd
http --json POST http://localhost:8000/api/movies/ title=Fargo genre=comedy year=1996
(2) script mode
# app/tests/movies/test_views.py import json import pytest from movies.models import Movie @pytest.mark.django_db def test_add_movie(client): movies = Movie.objects.all() assert len(movies) == 0 resp = client.post( "/api/movies/", { "title": "The Big Lebowski", "genre": "comedy", "year": "1998", }, content_type="application/json" ) assert resp.status_code == 201 assert resp.data["title"] == "The Big Lebowski" movies = Movie.objects.all() assert len(movies) == 1
Reminder: 之前是测试serializer,这里是测试url,然后 url --> serializer by view。
GET a Single Movie
一、添加 URL: REST API
路由分配。
# app/movies/urls.py from django.urls import path from .views import MovieList, MovieDetail urlpatterns = [ path("api/movies/", MovieList.as_view()), path("api/movies/<int:pk>/", MovieDetail.as_view()), # --> ]
GET 某一条内容,并处理。(如果是POST,则没有pk这个参数)
# app/movies/views.py
class MovieDetail(APIView):
def get_object(self, pk): try: return Movie.objects.get(pk=pk) except Movie.DoesNotExist: raise Http404 def get(self, request, pk, format=None): movie = self.get_object(pk) serializer = MovieSerializer(movie) return Response(serializer.data)
二、测试 REST API
每一次都要生成 Movie.objects,如果是多次测试,频繁创建就显得比较麻烦,所以,下面将会用到 fixture。
@pytest.mark.django_db def test_get_single_movie(client): movie = Movie.objects.create(title="The Big Lebowski", genre="comedy", year="1998") resp = client.get(f"/api/movies/{movie.id}/") assert resp.status_code == 200 assert resp.data["title"] == "The Big Lebowski" def test_get_single_movie_incorrect_id(client): resp = client.get(f"/api/movies/foo/") # 原本就没有的url,自然要返回404 assert resp.status_code == 404
GET All Movies
一、固件 Fixture
将 Movie.objects 的过程独立出来,并返回movie对象。
# app/tests/movies/conftest.py import pytest from movies.models import Movie @pytest.fixture(scope='function') def add_movie(): def _add_movie(title, genre, year): movie = Movie.objects.create(title=title, genre=genre, year=year) return movie return _add_movie
相应的 code 可以改进如下。
这里的add_movie其实后面省略了(),等价于函数的返回结果。故,既然都省略了(),也就默认不会有参数。
[改进后效果]
@pytest.mark.django_db def test_get_single_movie(client, add_movie):
movie = add_movie(title="The Big Lebowski", genre="comedy", year="1998")
resp = client.get(f"/api/movies/{movie.id}/") # 获取单个detail assert resp.status_code == 200 assert resp.data["title"] == "The Big Lebowski"
二、ORM --> Serialize --> Response
内部先添加两条信息。
然后再通过URL去获取这两条(全部)内容,之后通过 assert 进行测试。
@pytest.mark.django_db def test_get_all_movies(client, add_movie):
# 先添加 movie_one = add_movie(title="The Big Lebowski", genre="comedy", year="1998") movie_two = add_movie("No Country for Old Men", "thriller", "2007")
# 再获取 resp = client.get(f"/api/movies/") # 获取所有(目录)内容的 “具体过程” 如下。
assert resp.status_code == 200 assert resp.data[0]["title"] == movie_one.title assert resp.data[1]["title"] == movie_two.title
添加URL对应的GET处理方法,获取内容。
# app/movies/views.py class MovieList(APIView): def get(self, request, format=None): movies = Movie.objects.all() # 获得了数据 ORM serializer = MovieSerializer(movies, many=True) # 序列化后再发送返回值 Serialize return Response(serializer.data) # 返回 Response def post(self, request, format=None): serializer = MovieSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Database Seed
一、自动加载数据
[ { "model": "movies.movie", "pk": 1, "fields": { "title": "Fargo", "genre": "comedy", "year": "1996", "created_date": "2020-04-11T23:07:11.686Z", "updated_date": "2020-04-11T23:07:11.686Z" } }, { "model": "movies.movie", "pk": 2, "fields": { "title": "No Country for Old Men", "genre": "thriller", "year": "2007", "created_date": "2020-04-11T23:07:52.070Z", "updated_date": "2020-04-11T23:07:52.070Z" } }, { "model": "movies.movie", "pk": 3, "fields": { "title": "A Serious Man", "genre": "comedy", "year": "2009", "created_date": "2020-04-11T23:08:22.582Z", "updated_date": "2020-04-11T23:08:22.582Z" } } ]
导入数据并测试。
$ docker-compose exec movies python manage.py flush $ docker-compose exec movies python manage.py loaddata movies.json $ http --json http://localhost:8000/api/movies/
二、Pytest Commands
$ docker-compose exec movies pytest # disable warnings $ docker-compose exec movies pytest -p no:warnings # run only the last failed tests $ docker-compose exec movies pytest --lf # run only the tests with names that match the string expression $ docker-compose exec movies pytest -k "movie and not all_movies" # stop the test session after the first failure $ docker-compose exec movies pytest -x # enter PDB after first failure then end the test session $ docker-compose exec movies pytest -x --pdb # stop the test run after two failures $ docker-compose exec movies pytest --maxfail=2 # show local variables in tracebacks $ docker-compose exec movies pytest -l # list the 2 slowest tests $ docker-compose exec movies pytest --durations=2
End.