Django Restful接口开发与测试

Rest概述

简介

  • REST是英文Representational State Transfer(表现层状态转化),Rest是web服务的一种架构风格;
  • 使用HTTP,URI、XML、JSON、HTML等广泛流行的标准和协议;轻量级,跨平台、跨语言的架构设计;
  • 它是一种设计风格,不是一种标准,是一种思想。

Rest原则

  • 网络上的所有事物都可以被抽象为资源(resource)。
  • 每一个资源都有唯一的资源标识(resource identifier),对资源的操作不会改变这些标识
  • 所有的操作都是无状态的。 

设计思想

REST简化开发,其架构遵循CRUD原则,该原则告诉我们对于资源(包括网络资源)只需要四种行为:创建,获取,更新和删除就可以完成相关的操作和处理。

我们可以通过统一资源标识符(Universal Resource Identifier,URI)来识别和定位资源,并且针对这些资源而执行的操作是通过 HTTP 规范定义的。其核心操作只有GET,POST,PUT,DELETE。也就是:URL定位资源,用HTTP动词(GET,POST,DELETE,DETC)描述操作。

因此设计web接口的时候,REST主要是用于定义接口名,接口名一般是用名次写,不用动词,那怎么表达“获取”或者“删除”或者“更新”这样的操作呢——用请求类型(GET,PUT,POST,DELETE)来区分。

案例

比如我们设计一个用户管理系统的接口,如果不使用Restful风格,接口会定义如下:

1
2
3
4
5

http://127.0.0.1/user/query/1 #根据用户id查询用户数据 GET请求
http://127.0.0.1/user/adduser #新增用户 Post请求
http://127.0.0.1/user/update #修改用户信息 PUT请求
http://127.0.0.1/user/delete #删除用户信息 DELETE 请求

从上面的定义接口来看貌似没有什么问题,但是仔细揣测就会发现有一些瑕疵:比如查询方法定义接口使用了query动词,而GET请求本身的含义也就是从服务器获取资源,带有查询的含义,如果接口里面定义又加上这样的动词显得重复,同理其他几个接口也是一样的。

而使用Rest接口定义如下:

1
2
3
4
http://127.0.0.1/user/1  #GET 根据用户id查询用户数据
http://127.0.0.1/user #POST 新增用户
http://127.0.0.1/user #PUT 修改用户信息
http://127.0.0.1/user #DELETE 删除用户信息

从上面定义的接口我们可以看出,接口名称主要指向user资源,具体的资源操作(增删改查)由HTTP的请求类型来定义。这样接口名称显得统一整洁,就不用定义不同的接口名称。
遵循这样一种风格的Reset接口就叫做Restful

HTTP方法幂等性与安全性

HTTP方法 资源操作 幂等 安全
GET SELECT 安全
POST INSERT
PUT UPDATE
DELETE DELETE
  • 幂等:对同一Rest接口多次请求,得到的资源状态是相同的。
  • 安全:对该Rest接口请求,不会使服务器资源状态发生改变。

Rest优势

由于REST强制所有的操作都必须是无状态的,这就没有上下文的约束,如果做分布式,集群都不需要考虑上下文和会话保持的问题。极大的提高系统的可伸缩性。

前后端分离。前端拿到数据只负责展示和渲染,不对数据做任何处理。后端处理数据并以JSON格式传输出去,定义这样一套统一的接口,在web,ios,android三端都可以用相同的接口。

Django接口开发

Django简介

Django是一个开放源代码的Web应用框架,由Python写成。采用了MVC的框架模式,即模型M,视图V和控制器C。它最初是被开发来用于管理劳伦斯出版集团旗下的一些以新闻内容为主的网站的,即是CMS(内容管理系统)软件。并于2005年7月在BSD许可证下发布。这套框架是以比利时的吉普赛爵士吉他手Django Reinhardt来命名的。

Tips:Python的Web开发框架除了Django,还有FlaskTornado等。

Django安装

输入如下命令即可安装Django,注意需要提前配置好Python环境,这里选择的django版本是2.0.3版本,

1
pip install django

安装校验:在Windows命令提示符下输入django-admin命令回车。

提示如下内容则说明安装成功

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
C:\Users\Shuqing>django-admin

Type 'django-admin help <subcommand>' for help on a specific subcommand.

Available subcommands:

[django]
check
compilemessages
createcachetable
dbshell
diffsettings
dumpdata
flush
inspectdb
loaddata
makemessages
makemigrations
migrate
runserver
sendtestemail
shell
showmigrations
sqlflush
sqlmigrate
sqlsequencereset
squashmigrations
startapp
startproject
test
testserver
Note that only Django core commands are listed as settings are not properly configured (error: Requested setting INSTALLED_APPS, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.)

Django REST Framework

简介

Django REST Framework,是一套基于Django的REST风格的框架。

官方主页:http://www.django-rest-framework.org

特点

  • 功能强大、灵活,可以帮助你快速开发Web API。
  • 支持认证策略,包括OAuth1和OAuth2。
  • 支持ORM(对象关系映射)和非ORM数据源的序列化。
  • 丰富的文档以及良好的社区支持。

安装

1
2
3
pip install djangorestframework #Django REST Framework
pip install markdown # Markdown support for the browsable API.
pip install django-filter # Filtering support

创建API

项目创建

当Django REST Framework安装好之后,创建一个新的项目django_restful,如下命令所示我是创建在D盘根目录。在项目下创建api应用

1
2
3
C:\Users\Shuqing>d:
#创建一个新的项目
D:\>django-admin startproject django_restful

进入项目django_restful创建api应用,创建完成之后可以看到项目文件夹下面多了一个api文件夹

1
2
3
D:\>cd django_restful

D:\django_restful>python manage.py startapp api

进入到D:\django_rest\django_restful目录,打开settings.py在项目下创建apirest_framework

1
2
3
4
5
6
7
8
9
10
11
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'api',

]

rest_framework权限配置

默认的权限策略可以设置在全局范围内,通过DEFAULT_PERMISSION_CLASSES设置。在该文件末尾添加如下内容:

1
2
3
4
5
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}

数据库迁移

然后通过命令migrate命令进行数据库迁移。

1
D:\django_restful>python manage.py migrate

执行之后看到如下提示说明迁移成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying sessions.0001_initial... OK

创建超级管理员

使用createsuperuser命令来创建超级管理员账户,密码至少8位数字和字母组合。如51zxw20182018

1
2
3
4
5
6
D:\django_rest>python manage.py createsuperuser
Username (leave blank to use 'shuqing'): 51zxw
Email address: 51zxw@163.com
Password:
Password (again):
Superuser created successfully.

启动Server

启动django,然后验证登录我们创建的超级管理员账户。

1
2
3
4
5
6
7
8
D:\django_rest>python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
July 20, 2018 - 16:01:39
Django version 2.0.7, using settings 'django_rest.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

启动成功之后,输入地址:http://127.0.0.1:8000/ 我们可以看到如下页面:

django-default-index

Tips:也可以自定义hostport,如下所示:

1
python manage.py runserver 127.0.0.1:8001

登录超级管理员账户

我们再输入地址:http://127.0.0.1:8000/admin 即可进入到登录超级管理员账户界面:

django-admin-login

登录我们创建的账户,可以看到如下界面:

django administration

数据序列化

Serializers用于定义API的表现形式,如返回哪些字段、返回怎样的格式等。这里序列化Django自带的UserGroup。创建数据序列化,在api应用下创建serializers.py文件。

serializers-file

序列化代码如下:

serializers.py

1
2
3
4
5
6
7
8
9
10
11
12
from django.contrib.auth.models import User,Group
from rest_framework import serializers

class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model=User
fields=('url','username','email','groups')

class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model=Group
fields=('url','name')

创建视图

视图用于如何向用户展示数据,展示哪些数据。比如用户查询User信息或查询Group信息,那么程序内部要定义好怎么去查询。在Django REST framework中,ViewSets用于定义视图的展现形式,例如返回哪些内容,需要做哪些权限处理。

打开api应用下的views.py文件,编写如下代码:

views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
from django.shortcuts import render
from django.contrib.auth.models import User,Group
from rest_framework import viewsets
from api.serializers import UserSerializer,GroupSerializer

# Create your views here.
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer

class GroupViewSet(viewsets.ModelViewSet):
queryset = Group.objects.all()
serializer_class = GroupSerializer

在URL中会定义相应的规则到ViewSetsViewSets则通过serializer_class找到对应的Serializers

这里将UserGroup的所有对象赋予queryset,并返回这些值。在UserSerializerGroupSerializer中定义要返回的字段。

URL路由配置

打开.../django_rest/urls.py文件,添加api的路由配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from django.contrib import admin
from django.urls import path
from django.conf.urls import include
from rest_framework import routers
from api import views


router=routers.DefaultRouter()
router.register(r'users',views.UserViewSet)
router.register(r'groups',views.GroupViewSet)



urlpatterns = [
path('admin/', admin.site.urls),
path('',include(router.urls)),
path('api-auth/',include('rest_framework.urls',namespace='rest_framework')),

]

注意:Django1.0和2.0关于路由配置写法有一些区别,使用1.0路由配置如下

1
2
3
4
5
6
7
from django.conf.urls import url

urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^',include(router.urls)),
url(r'^api-auth/',include('rest_framework.urls',namespace='rest_framework')),
]

打开API主页

启动服务,然后在浏览器打开:http://127.0.0.1:8000/ 即可看到API主页,如下所示:

image

创建数据

点击主页右上角的Log in登录超级管理员账户,然后分别创建GroupUser数据。

userList

Tips: Vary: Accept标头可告诉代理服务器缓存两种版本的资源:压缩和非压缩,这有助于避免一些公共代理不能正确地检测Content-Encoding标头的问题。

group_list

Swagger接口文档生成

接口开发完成了,那么接下来需要编写接口文档。传统的接口文档编写都是使用Word或者其他一些接口文档管理平台,这种形式接口文档维护更新比较麻烦,每次接口有变动时得手动修改文档。因此,针对这种情况,这里推荐使用Swagger来管理接口文档。

Swagger简介

Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。总体目标是使客户端和文件系统(源代码)作为服务器以同样的速度来更新。每当接口有变动时,对应的接口文档也会自动更新。

Tips:http://httpbin.org/#/ 也是利用Swagger生成接口文档。

Swagger能成为最受欢迎的REST APIs文档生成工具之一,有以下几个原因:

  • Swagger 可以生成一个具有互动性的API控制台,开发者可以用来快速学习和尝试API。
  • Swagger 可以生成客户端SDK代码用于各种不同的平台上的实现。
  • Swagger 文件可以在许多不同的平台上从代码注释中自动生成。
  • Swagger 有一个强大的社区,里面有许多强悍的贡献者。

Django 接入Swagger

首先安装 django-rest-swagger

1
pip install django-rest-swagger

进入到setting.py文件,添加django-rest-swagger应用

1
2
3
4
5
6
7
8
9
10
11
12
13
# Application definition

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'api',
'rest_framework_swagger',
]

进入到views.py将之前定义的UserViewSetGroupViewSet补充注释:

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

from django.contrib.auth.models import User,Group
from rest_framework import viewsets
from api.serializers import UserSerializer,GroupSerializer

# Create your views here.
class UserViewSet(viewsets.ModelViewSet):
"""
retrieve:
Return a user instance.

list:
Return all users, ordered by most recently joined.

create:
Create a new user.

delete:
Remove an existing user.

partial_update:
Update one or more fields on an existing user.

update:
Update a user.
"""
queryset = User.objects.all()
serializer_class = UserSerializer

class GroupViewSet(viewsets.ModelViewSet):
"""
retrieve:
Return a group instance.

list:
Return all groups, ordered by most recently joined.

create:
Create a new group.

delete:
Remove an existing group.

partial_update:
Update one or more fields on an existing group.

update:
Update a group.
"""
queryset = Group.objects.all()
serializer_class = GroupSerializer

urls.py 添加get_schema_view辅助函数

1
2
3
4
5
6
7
8
9
10
11
from rest_framework.schemas import get_schema_view
from rest_framework_swagger.renderers import SwaggerUIRenderer,OpenAPIRenderer

schema_view=get_schema_view(title='API',renderer_classes=[OpenAPIRenderer,SwaggerUIRenderer])

urlpatterns = [
path('admin/', admin.site.urls),
path('',include(router.urls)),
path('api-auth/',include('rest_framework.urls',namespace='rest_framework')),
path('docs/',schema_view,name='docs')
]

启动服务,然后打开地址:http://127.0.0.1:8000/docs/ 即可看到如下界面:

swagger-index

分别点击groupsusers即可看到自动生成的接口文档。

swagger-groups

swagger-user

Restful接口测试

开发完接口,接下来我们需要对我们开发的接口进行测试。接口测试的方法比较多,使用接口工具或者Python来测试都可以,工具方面比如之前我们学习过的Postman或者Jmeter ,Python脚本测试可以使用Requests + unittest来测试。

测试思路

  • 功能测试:数据的增删改查
  • 异常测试:未授权,参数异常等

Postman测试

使用测试工具Postman测试结果如下所示:

user接口测试

查询所有用户

postman-users

创建用户

postman-adduser

修改用户

postman-update-user

删除用户

postman-delete-user

未授权测试

postman-noAuth

groups接口测试

查询所有groups数据

postman-groups

修改group数据

postman-update-groups

删除groups

postman-delete-groups

Requests+Unittest

api目录下面新建一个test_unittest.py,代码实现如下:

tests_unittest.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

import requests
import unittest

class UserTest(unittest.TestCase):
def setUp(self):
self.base_url='http://127.0.0.1:8000/users'
self.auth=('51zxw','zxw20182018')

def test_get_user(self):
r=requests.get(self.base_url+'/1/',auth=self.auth)
result=r.json()

self.assertEqual(result['username'],'51zxw')
self.assertEqual(result['email'],'51zxw@163.com')


def test_add_user(self):
form_data={'username':'zxw222','email':'zxw668@qq.com','groups':'http://127.0.0.1:8000/groups/2/'}
r=requests.post(self.base_url+'/',data=form_data,auth=self.auth)
result=r.json()

self.assertEqual(result['username'],'zxw222')


def test_delete_user(self):
r=requests.delete(self.base_url+'/11/',auth=self.auth)

self.assertEqual(r.status_code,204)

def test_update_user(self):
form_data={'email':'2222@163.com'}
r=requests.patch(self.base_url+'/2/',auth=self.auth,data=form_data)
result=r.json()

self.assertEqual(result['email'],'2222@163.com')


def test_no_auth(self):
r=requests.get(self.base_url)
result=r.json()

self.assertEqual(result['detail'],'Authentication credentials were not provided.')

class GroupTest(unittest.TestCase):
def setUp(self):
self.base_url='http://127.0.0.1:8000/groups'
self.auth=('51zxw','zxw20182018')

def test_group_developer(self):
r=requests.get(self.base_url+'/7/',auth=self.auth)
result=r.json()

self.assertEqual(result['name'],'Developer')

def test_add_group(self):
form_data={'name':'Pm'}
r=requests.post(self.base_url+'/',auth=self.auth,data=form_data)
result=r.json()

self.assertEqual(result['name'],'Pm')

def test_update_group(self):
form_data={'name':'Boss'}
r=requests.patch(self.base_url+'/6/',auth=self.auth,data=form_data)
result=r.json()

self.assertEqual(result['name'],'Boss')

def test_detele_group(self):
r=requests.delete(self.base_url+'/6/',auth=self.auth)

self.assertEqual(r.status_code,204)


if __name__ == '__main__':
unittest.main()

Django自带测试模块

打开api目录下面的tests文件,编写如下测试代码

tests.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
80
81

from django.test import TestCase
import requests

# Create your tests here.
class UserTest(TestCase):
def setUp(self):
self.base_url='http://127.0.0.1:8000/users'
self.auth=('51zxw','xxxxx')

def test_get_user(self):
r=requests.get(self.base_url+'/1/',auth=self.auth)
result=r.json()

self.assertEqual(result['username'],'51zxw')
self.assertEqual(result['email'],'zxw886@qq.com')

# @unittest.skip('skip add user')
def test_add_user(self):
form_data={'username':'zxw222','email':'zxw668@qq.com','groups':'http://127.0.0.1:8000/groups/2/'}
r=requests.post(self.base_url+'/',data=form_data,auth=self.auth)
result=r.json()

self.assertEqual(result['username'],'zxw222')

# @unittest.skip('skip test_delete_user')
def test_delete_user(self):
r=requests.delete(self.base_url+'/11/',auth=self.auth)

self.assertEqual(r.status_code,204)

def test_update_user(self):
form_data={'email':'2222@163.com'}
r=requests.patch(self.base_url+'/2/',auth=self.auth,data=form_data)
result=r.json()

self.assertEqual(result['email'],'2222@163.com')

def test_user_already_exists(self):
form_data = {'username': 'zxw222', 'email': 'zxw668@qq.com', 'groups': 'http://127.0.0.1:8000/groups/2/'}
r = requests.post(self.base_url + '/', data=form_data, auth=self.auth)
result = r.json()
#预期返回值:{"username":["A user with that username already exists."]}
self.assertEqual(result['username'][0], 'A user with that username already exists.')

def test_no_auth(self):
r=requests.get(self.base_url)
result=r.json()

self.assertEqual(result['detail'],'Authentication credentials were not provided.')

class GroupTest(TestCase):
def setUp(self):
self.base_url='http://127.0.0.1:8000/groups'
self.auth=('51zxw','xxxxxx')

def test_group_developer(self):
r=requests.get(self.base_url+'/3/',auth=self.auth)
result=r.json()

self.assertEqual(result['name'],'Pm')

# @unittest.skip('skip test_add_group')
def test_add_group(self):
form_data={'name':'Leader'}
r=requests.post(self.base_url+'/',auth=self.auth,data=form_data)
result=r.json()

self.assertEqual(result['name'],'Leader')

def test_update_group(self):
form_data={'name':'Boss'}
r=requests.patch(self.base_url+'/6/',auth=self.auth,data=form_data)
result=r.json()

self.assertEqual(result['name'],'Boss')

def test_detele_group(self):
r=requests.delete(self.base_url+'/6/',auth=self.auth)

self.assertEqual(r.status_code,204)

运行方式:打开cmd使用如下命令来运行即可:

1
D:\django_restful>python manage.py test

上面命令是默认测试全部的用例,如果想测试部分用例则可以使用如下命令:

测试指定的测试类

1
D:\django_restful>python manage.py test api.tests.UserTest

测试具体的某一条具体用例

1
D:\django_restful>python manage.py test api.tests.UserTest.test_get_user

视频操作演示

《Python接口自动化测试教程》5-1~5-11

报错相关

  1. 迁移数据库时没有权限写入
1
2
3
File "C:\Users\jli75\AppData\Local\Programs\Python\Python37\lib\site-packages\MySQLdb\connections.py", line 280, in query
_mysql.connection.query(self, query)
django.db.utils.InternalError: (7, "Error on rename of '.\\httprunnermanager\\#sql-1178_7.frm' to '.\\httprunnermanager\\djcelery_taskstate.frm' (Errcode: 13 - Permission denied)")

原因:可能是杀毒软件通过阻止修改frm文件来解决此问题。 通过在杀毒软件威胁防护高级选项中禁用按访问扫描,并杀毒软件设置为忽略这些扩展名来解决此问题

  1. 迁移数据库时没有清除之前的迁移文件migrations
    1
    2
    3
      File "C:\Users\jli75\AppData\Local\Programs\Python\Python37\lib\site-packages\MySQLdb\connections.py", line 280, in query
    _mysql.connection.query(self, query)
    _mysql_exceptions.OperationalError: (1050, "Table 'djcelery_crontabschedule' already exists")

解决方案:删除migrations文件夹即可。

  1. setting配置错误
1
2
raise MigrationSchemaMissing("Unable to create the django_migrations table (%s)" % exc)
django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table ((1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(6) NOT NULL)' at line 1"))

解决方案:
Django2.1不再支持MySQL5.5,必须5.6版本以上
可以使用如下命令 查看当前Mysql版本

1
2
mysql -V
mysql Ver 8.0.1-dmr for Win64 on x86_64 (MySQL Community Server (GPL))

参考资料