Python Django – 使用 DRF 和 Docker 测试驱动的 Web API 开发
我们将使用Django rest 框架、 docker创建一个待办事项列表 Web API,并使用测试驱动开发为我们的代码中的不同功能编写不同的测试,但让我们首先看看这个项目的先决条件是什么。
先决条件:
- 安装在本地系统上的 Docker
- Python 3.0基础知识
- Django 3.0基础知识
现在我们准备好了,让我们进一步了解 docker 和测试驱动开发 (TDD) 以及为什么要使用它们。
码头工人:
Docker 是一个开源容器化平台,用于自动将应用程序部署为可在云或本地运行的可移植、自给自足的容器。
考虑这样一种情况,软件工程师编写代码并将其发送以进行测试,但该代码不会在测试人员的本地环境中运行,因为所有依赖项都没有得到满足,只需使用 docker 即可解决此问题。
测试驱动开发(TDD):
测试驱动开发是一种软件开发实践,其重点是在编写实际代码之前编写单元测试,它是一种结合了编程、单元测试的创建和重构的迭代方法。
单元测试:单元测试是测试我们代码中不同功能的测试
编写测试的三个关键步骤:
- 设置:创建将在不同测试中使用的示例函数
- 执行:调用正在测试的代码
- 断言:将结果与预期结果进行比较。
现在让我们转到实际的建筑部分。
创建项目并设置 Dockerfile:
按照以下步骤创建项目并设置Dockerfile。
第 1 步:创建 Dockerfile。
- 创建文件名Dockerfile。 Dockerfile 是一个包含构建 Docker 映像的指令的文件。
第 2 步:填充 Dockerfile。
FROM python:3.8-alpine
ENV PYTHONBUFFERED=1
- 我们的dockerfile的第一行指定了我们将在其上构建我们的镜像的现有Python镜像我们将使用Python:3.8 alpine 镜像作为我们的基础镜像,如果你愿意,你可以创建自己的基础镜像,使用的原因alpine 是轻量级的,建造时间更短
- 接下来将环境pythonbuffered设置为 1 这可以防止输出缓冲并减少构建时间
COPY ./requirements.txt /requirements.txt
RUN pip3 install -r requirements.txt
- 此命令意味着将我们的本地 requirements.txt 复制到我们的图像 requirements.txt
- 运行我们的需求文件来安装所有依赖项
RUN mkdir /app
WORKDIR /app
COPY ./app /app
RUN adduser -D user
USER user
- 我们将保留名为 app 的工作目录并将本地 app 目录复制到映像中。
- 为我们的图像创建一个新用户,这样我们就不会使用 root 作为我们的主要用户是一种安全做法
第 3 步:创建一个项目文件夹。
- 在 Dockerfile 之外创建一个名为 app 的文件夹。这将是我们的项目文件夹。
第 4 步:创建requirements.txt。
- 该文件包含我们项目所需的依赖项,将以下内容添加到您的requirements.txt
Django==3.2.12
djangorestframework==3.13.1
第 5 步:构建 Docker 映像。
$ docker build .
运行上面的命令,你的 docker 镜像必须开始构建
第 6 步:创建 docker-compose。
- 在 Dockerfile 之外创建文件名 docker- compose.yml。 Docker-compose 文件是包含我们在应用程序中实现的不同服务的文件,还包含设置和运行这些服务的不同指令。
- 将此包含在您的 docker-compose.yml 中
XML
version: "3"
services:
app:
build:
context: .
ports:
- "8000:8000"
volumes:
- ./app:/app
command: >
sh -c "python3 manage.py runserver 0.0.0.0:8000"
Python3
from django.test import TestCase
from api import models
class ModelTest(TestCase):
def test_tag_model_str(self):
task = models.Task.objects.create(
task_name = "Do homework",
task_description = "Need to complete the homework today",
task_iscompleted = False
)
self.assertEqual(str(task), task.task_name)
Python3
from django.db import models
class Task(models.Model):
task_name = models.CharField(max_length=255)
task_description = models.TextField()
task_time = models.DateTimeField(auto_now_add=True)
task_iscompleted = models.BooleanField()
def __str__(self):
return self.task_name
Python3
from django.contrib import admin
from .models import Task
admin.site.register(Task)
Python3
from rest_framework import serializers
from .models import Task
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = '__all__'
Python3
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from django.urls import reverse
from api.serializers import TaskSerializer
from api.models import Task
get_tasks_url = reverse('get')
create_url = reverse('create')
def update_url(id):
return reverse('update', args=[id])
def delete_url(id):
return reverse('delete', args=[id])
def details_url(id):
return reverse('details', args=[id])
def sample_payload():
payload = {
"task_name" : "Do homework",
"task_description" : "Need to complete the homework today",
"task_iscompleted" : False
}
return Task.objects.create(**payload)
Python3
class TaskApiTest(TestCase):
def setUp(self):
self.client = APIClient()
def test_get_task_list(self):
res = self.client.get(get_tasks_url)
self.assertEqual(res.status_code, status.HTTP_200_OK)
def test_create_task(self):
payload = {
"task_name" : "Do homework",
"task_description" : "Need to complete the homework today",
"task_iscompleted" : False
}
res = self.client.post(create_url, payload)
self.assertEqual(res.status_code, status.HTTP_201_CREATED)
def test_update_task(self):
task = sample_payload()
payload = {
"task_name" : "Do homework",
"task_description" : "Need to complete the homework today",
"task_iscompleted" : True
}
url = update_url(task.id)
res = self.client.put(url, payload)
task.refresh_from_db()
self.assertEqual(res.status_code, status.HTTP_201_CREATED)
def test_delete_task(self):
task = sample_payload()
url = delete_url(task.id)
res = self.client.delete(url)
self.assertEqual(res.status_code, status.HTTP_202_ACCEPTED)
def test_task_details(self):
task = sample_payload()
url = details_url(task.id)
res = self.client.get(url)
self.assertEqual(res.status_code, status.HTTP_200_OK)
Python3
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from .models import Task
from .serializers import TaskSerializer
@api_view(['GET'])
def get_all_tasks(request):
tasks = Task.objects.all().order_by("-task_time")
serializer = TaskSerializer(tasks, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@api_view(['GET'])
def get_task_details(request, pk):
task = Task.objects.get(id=pk)
serializer = TaskSerializer(task, many=False)
return Response(serializer.data, status=status.HTTP_200_OK)
Python3
@api_view(['POST'])
def create_task(request):
serializer = TaskSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(status=status.HTTP_400_BAD_REQUEST)
Python3
@api_view(['PUT'])
def update_task(request, pk):
task = Task.objects.get(id=pk)
serializer = TaskSerializer(instance=task, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(status=status.HTTP_400_BAD_REQUEST)
@api_view(['DELETE'])
def delete_task(request, pk):
task = Task.objects.get(id=pk)
task.delete()
return Response('Deleted successfully',status=status.HTTP_202_ACCEPTED)
Python3
from django.urls import path
from . import views
urlpatterns = [
path('get/', views.get_all_tasks, name="get"),
path('create/', views.create_task, name="create"),
path('update/', views.update_task, name="update"),
path('delete/', views.delete_task, name="delete"),
path('details/', views.get_task_details, name="details")
]
Python3
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls'))
]
注意:我们使用的是 docker-compose 版本 3,我们的应用程序包含一个名为 app 的服务,该服务存储在名为app的文件夹中,我们将使用端口 8000 使用上述命令运行它。
第 7 步:构建 docker-compose。
- 运行以下命令开始构建您的 docker-compose。
$ docker-compose build
第 8 步:创建 Django 项目:
$ docker-compose run app sh -c "django-admin startproject app ."
第 9 步:运行 Django 服务器。
- 运行以下命令启动您的 Django 应用程序并前往 https://127.0.0.1:8000 以检查您的应用程序是否已启动
$ docker-compose up
第 10 步:创建一个名为“api”的 Django 应用程序。
$ cd app
$ docker-compose run app sh -c "python manage.py startapp api"
- 这将创建一个 API 应用程序,我们将在其中创建存储所有 API CRUD 操作以及我们对api应用程序的测试。现在转到app>settings.py并在installed_apps 添加“api”、“rest_framework”作为新应用程序。
现在我们已经建立了我们的项目,我们可以开始编写实际代码,但首先让我们了解如何在Python Django 中编写测试。
在 Django 中编写测试的规则:
编写 Django 测试时需要遵循以下规则:
- 规则 1:在应用程序中创建一个名为tests的文件夹。在这个文件夹中,我们将存储测试删除,已经存在的 tests.py 文件
- 规则 2:在测试文件夹中创建__init__.py
- 规则 3:创建不同的Python文件来测试不同的部分,在我们的例子中,例如,不同的文件用于测试模型和视图。请记住,对于test_model.py ,每个文件名都必须从“test”开始。
- 规则 4:每个文件必须包含一个包含不同单元测试的类,作为测试不同功能的函数,函数名称必须以“test”开头,例如: def test_<功能名称>()
为测试任务模型编写测试:
按照以下步骤编写任务模型的测试:
第 1 步:创建一个test_models。
- 在 API 应用程序的测试文件夹中,创建一个名为test_models.py的文件,该文件将存储与我们将要创建的模型相关的测试。
第 2 步:编写第一个模型测试。
Python3
from django.test import TestCase
from api import models
class ModelTest(TestCase):
def test_tag_model_str(self):
task = models.Task.objects.create(
task_name = "Do homework",
task_description = "Need to complete the homework today",
task_iscompleted = False
)
self.assertEqual(str(task), task.task_name)
- 上面我们导入了所需的模块,我们还没有创建我们的模型,但我们将在一秒钟内创建它们。
- 创建类 ModelTest() 并用 TestCase 扩展它
- 在这里,我们创建了一个单元测试来测试模型,并编写了一个断言来检查输出结果是否与使用 assertEqual() 比较两者的预期结果相同。
创建任务模型:
按照以下步骤创建任务模型:
第 1 步:在我们的 API 应用程序的models.py文件中包含以下代码:
Python3
from django.db import models
class Task(models.Model):
task_name = models.CharField(max_length=255)
task_description = models.TextField()
task_time = models.DateTimeField(auto_now_add=True)
task_iscompleted = models.BooleanField()
def __str__(self):
return self.task_name
- 创建一个类并使用models.Model对其进行扩展。
- 编写代表模型列的不同类字段。
- 最后,我们返回 task_name 进行验证。
第 2 步:注册任务模型。
- 前往 admin.py 并使用以下代码注册模型:
Python3
from django.contrib import admin
from .models import Task
admin.site.register(Task)
第 3 步:迁移更改。
- 是时候迁移我们的更改了(确保每次更改/创建模型时都进行迁移)。首先,使用
$ docker-compose run app sh -c "python manage.py makemigrations"
- 现在迁移。
docker-compose run app sh -c "python manage.py migrate"
第 4 步:创建模型序列化程序。
- 在 API 应用程序中为我们的模型创建一个序列化程序,创建一个名为serializers.py的文件,并包含用于验证传入请求数据的序列化程序。
Python3
from rest_framework import serializers
from .models import Task
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = '__all__'
第 5 步:测试 t ask模型。
- 运行以下命令:
docker-compose run app sh -c "python manage.py test"
- 确保所有测试都通过。
- 现在我们已经创建了模型,是时候编写 API 视图了。
为 API 视图编写测试:
按照以下步骤编写 API 视图的测试:
第一步:创建一个测试文件。
- 前往 API 应用程序中的 tests 文件夹并创建文件test_task_api.py ,这是我们要为测试 API 编写测试的地方。
第 2 步:编写测试。
- 以下代码包含针对不同 API 操作的单元测试:
Python3
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from django.urls import reverse
from api.serializers import TaskSerializer
from api.models import Task
get_tasks_url = reverse('get')
create_url = reverse('create')
def update_url(id):
return reverse('update', args=[id])
def delete_url(id):
return reverse('delete', args=[id])
def details_url(id):
return reverse('details', args=[id])
def sample_payload():
payload = {
"task_name" : "Do homework",
"task_description" : "Need to complete the homework today",
"task_iscompleted" : False
}
return Task.objects.create(**payload)
上面我们导入了所需的模块,然后我们使用反向函数,它允许通过那里提供的名称-值从urls.py文件中检索 URL 详细信息,然后我们创建sample_payload这是一个示例任务模型对象,它创建示例函数,使我们的代码干净和快速地。
Python3
class TaskApiTest(TestCase):
def setUp(self):
self.client = APIClient()
def test_get_task_list(self):
res = self.client.get(get_tasks_url)
self.assertEqual(res.status_code, status.HTTP_200_OK)
def test_create_task(self):
payload = {
"task_name" : "Do homework",
"task_description" : "Need to complete the homework today",
"task_iscompleted" : False
}
res = self.client.post(create_url, payload)
self.assertEqual(res.status_code, status.HTTP_201_CREATED)
def test_update_task(self):
task = sample_payload()
payload = {
"task_name" : "Do homework",
"task_description" : "Need to complete the homework today",
"task_iscompleted" : True
}
url = update_url(task.id)
res = self.client.put(url, payload)
task.refresh_from_db()
self.assertEqual(res.status_code, status.HTTP_201_CREATED)
def test_delete_task(self):
task = sample_payload()
url = delete_url(task.id)
res = self.client.delete(url)
self.assertEqual(res.status_code, status.HTTP_202_ACCEPTED)
def test_task_details(self):
task = sample_payload()
url = details_url(task.id)
res = self.client.get(url)
self.assertEqual(res.status_code, status.HTTP_200_OK)
上面我们对不同的视图有不同的单元测试,首先我们创建一个类并用 TestCase 扩展它并编写一个函数来测试不同的视图(注意每个函数名称如何以 test 开头),从 TestCase 覆盖 setup函数并使用它来初始化类变量,然后编写代码并执行 require 断言以将给定的输出与预期的输出进行比较。
编写 API 视图和 URL:
按照以下步骤编写 API 视图和 URL:
第 1 步:创建视图
- 前往views.py并编写以下代码。下面的视图用于处理不同的请求并为我们的 API 生成所需的输出,这里我们使用基于函数的视图和 api_view 装饰器。此功能的核心是 api_view 装饰器,它采用您的视图应响应的 HTTP 方法列表。
- 为了发送我们使用 Response() 的输出,与常规的 HttpResponse 对象不同,您不使用呈现的内容来实例化 Response 对象。相反,您传入未渲染的数据,这些数据可能包含任何Python原语。
Python3
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from .models import Task
from .serializers import TaskSerializer
@api_view(['GET'])
def get_all_tasks(request):
tasks = Task.objects.all().order_by("-task_time")
serializer = TaskSerializer(tasks, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@api_view(['GET'])
def get_task_details(request, pk):
task = Task.objects.get(id=pk)
serializer = TaskSerializer(task, many=False)
return Response(serializer.data, status=status.HTTP_200_OK)
- 在这里,我们导入了所需的模块并编写了第一个视图以获取已创建任务的列表,首先我们使用 Task 模型获取任务,然后我们序列化数据并返回带有数据和 200 个状态码的响应。
- 然后我们创建了一个视图来使用它的 id 获取单个任务的详细信息。
Python3
@api_view(['POST'])
def create_task(request):
serializer = TaskSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(status=status.HTTP_400_BAD_REQUEST)
上面是创建新任务的视图,首先我们需要从请求中获取数据并序列化它,如果序列化数据有效,然后只需保存序列化程序并返回所需的响应。
Python3
@api_view(['PUT'])
def update_task(request, pk):
task = Task.objects.get(id=pk)
serializer = TaskSerializer(instance=task, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(status=status.HTTP_400_BAD_REQUEST)
@api_view(['DELETE'])
def delete_task(request, pk):
task = Task.objects.get(id=pk)
task.delete()
return Response('Deleted successfully',status=status.HTTP_202_ACCEPTED)
- 接下来是更新和删除任务,更新接受待更新任务的 id,然后检查序列化程序的有效性并返回响应,与删除视图相同,从请求中获取任务对象 id 并删除任务并返回响应。
在这里,我们完成了编写视图。
第 2 步:配置 URL。
- 如果没有,则转到 API 应用程序中的urls.py创建 urls.py 并包含以下代码:
Python3
from django.urls import path
from . import views
urlpatterns = [
path('get/', views.get_all_tasks, name="get"),
path('create/', views.create_task, name="create"),
path('update/', views.update_task, name="update"),
path('delete/', views.delete_task, name="delete"),
path('details/', views.get_task_details, name="details")
]
以上是向不同视图发出请求并获得响应的不同 URL 模式。
- 此外,在主项目文件夹 app goto urls.py 中的 goto urls.py 和 URL 模式中添加:
Python3
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls'))
]
在这里,如果任何 URL 从api/开始,则将控制权转移到api.urls。
最后一个考试:
- 前往终端并运行以下命令:
docker-compose run app sh -c "python manage.py test"
- 确保所有测试都通过:
输出:
- 当您使用以下命令运行服务器时:
$ docker-compose up
- 创建任务:
- 获取所有任务:
- 更新任务:
- 删除任务:
- 获取任务详情:
结论:
因此,我们了解了使用 Django REST 框架、docker 和测试驱动开发来创建 API。由于软件行业发展如此之快,因此有必要使用新技术进行更新。