五、【API 开发篇(下)】:使用 Django REST Framework构建测试用例模型的 CRUD API

【API 开发篇】:使用 Django REST Framework构建测试用例模型的 CRUD API

    • 前言
      • [第一步:增强 Serializers (序列化器) - 处理关联和选择项](#第一步:增强 Serializers (序列化器) - 处理关联和选择项)
      • [第二步:创建 TestCaseViewSet (视图集) - 支持过滤](#第二步:创建 TestCaseViewSet (视图集) - 支持过滤)
      • [第三步:注册 TestCaseViewSet 到 Router](#第三步:注册 TestCaseViewSet 到 Router)
      • [第四步:测试 TestCase API](#第四步:测试 TestCase API)
    • 总结

前言

在上一篇项目与模块的 API 开发中,我们掌握了 ModelSerializerModelViewSet 的基本用法,并利用 DefaultRouter 快速生成了 URL。

对于 TestCase 模型,我们可能会遇到以下需求:

  1. 关联数据显示: 在获取测试用例列表或详情时,我们可能希望不仅仅看到所属模块的 ID (module_id),还想直接看到模块的名称,甚至项目的名称。
  2. 写入时处理关联: 创建或更新测试用例时,前端可能会传递模块的 ID,后端需要正确处理这种关联。
  3. 更复杂的字段处理: TestCase 模型中有 choices 类型的字段 (如 priority, case_type),还有可能需要特殊处理的文本字段 (如 steps_text)。
  4. 特定业务逻辑的过滤: 例如,我们可能需要根据项目 ID 来筛选测试用例,或者根据模块 ID 来筛选测试用例。

第一步:增强 Serializers (序列化器) - 处理关联和选择项

我们需要为 TestCase 模型创建一个序列化器,并考虑如何更好地展示关联数据和处理选择项。

打开 api/serializers.py 文件,在 ModuleSerializer 之后添加 TestCaseSerializer

python 复制代码
# test-platform/api/serializers.py

from rest_framework import serializers
from .models import Project, Module, TestCase

# ... (ProjectSerializer 和 ModuleSerializer 代码保持不变) ...

class TestCaseSerializer(serializers.ModelSerializer):
    """
    测试用例序列化器
    """
    # 1. 显示关联对象的详细信息 (只读)
    # 使用 SerializerMethodField 来自定义序列化输出
    module_name = serializers.CharField(source='module.name', read_only=True)
    project_name = serializers.CharField(source='module.project.name', read_only=True)
    project_id = serializers.IntegerField(source='module.project.id', read_only=True) # 方便前端筛选

    # 2. 对于 choices 字段,我们可以让前端直接看到可选项的描述文本 (只读)
    # DRF 默认会返回 choice 的实际存储值 (如 'P0')
    # 如果需要返回描述文本 (如 'P0 - 最高'),可以使用 `SerializerMethodField` 或 `ChoiceField`
    # 这里我们选择在前端处理显示,后端保持原始值,但可以添加一个 `get_xxx_display` 的方法到模型中,DRF 会自动识别
    # 或者,更简单的方式是,让前端直接获取这些 choices,这里我们暂时保持默认。
    # 如果想在序列化时直接获得 display 值,可以这样做:
    priority_display = serializers.CharField(source='get_priority_display', read_only=True)
    case_type_display = serializers.CharField(source='get_case_type_display', read_only=True)


    class Meta:
        model = TestCase
        # fields = '__all__' # 默认会包含 module (仅ID), create_time, update_time 等
        # 我们明确指定字段,并包含自定义的只读字段
        fields = [
            'id', 'name', 'description', 'module', 'module_name', 'project_id', 'project_name',
            'priority', 'priority_display', 'precondition', 'steps_text', 'expected_result',
            'case_type', 'case_type_display', 'maintainer',
            'create_time', 'update_time'
        ]
        # 3. 写入时只接受 module_id
        # read_only_fields 用于指定哪些字段仅在序列化输出时显示,不在反序列化(创建/更新)时接受输入
        # 我们已经在自定义字段上加了 read_only=True,这里可以不用再写
        # read_only_fields = ['module_name', 'project_name', 'project_id', 'priority_display', 'case_type_display', 'create_time', 'update_time']

        # 如果想在创建/更新时只允许传入 module 的 id,而 module_name 等是只读的,
        # 并且希望在API文档中明确,可以像下面这样配置 extra_kwargs
        extra_kwargs = {
            'create_time': {'read_only': True},
            'update_time': {'read_only': True},
            'module': {'write_only': False, 'help_text': "关联的模块ID"}, # module 字段本身可读可写 (ID)
        }

代码解释:

  1. 显示关联对象的名称 (如 module_name, project_name):

    • module_name = serializers.CharField(source='module.name', read_only=True):
      • 我们定义了一个新的字段 module_name
      • source='module.name' 告诉 DRF 这个字段的值应该从当前 TestCase 实例的 module 属性的 name 属性获取 (即 testcase_instance.module.name)。这利用了 Django 模型反向查询的特性。
      • read_only=True 表示这个字段只用于序列化输出(即 GET 请求的响应),不能用于反序列化输入(即 POST 或 PUT 请求的请求体)。当我们创建或更新测试用例时,我们仍然通过传递 module 字段(模块的 ID)来指定其所属模块。
    • project_name = serializers.CharField(source='module.project.name', read_only=True): 类似地,获取项目名称。
    • project_id = serializers.IntegerField(source='module.project.id', read_only=True): 获取项目ID,方便前端进行筛选或构建链接。
  2. 显示 Choices 字段的描述文本 (如 priority_display):

    • priority_display = serializers.CharField(source='get_priority_display', read_only=True):
      • Django 模型字段如果定义了 choices,会自动拥有一个 get_FIELD_display() 方法(例如,priority 字段有 get_priority_display() 方法)。这个方法会返回该字段当前值的可读描述。
      • 通过 source='get_priority_display',我们可以直接在序列化器中调用这个方法来获取描述文本。
      • read_only=True 同样表示这是只读的。
  3. Meta 类中的配置:

    • fields = [...]: 我们明确列出了所有希望在 API 中暴露的字段,包括我们自定义的只读字段。
    • extra_kwargs:
      • 'module': {'write_only': False, 'help_text': "关联的模块ID"}:
        • write_only=False (默认值) 意味着 module 字段(它代表模块的 ID)在读取和写入时都有效。
        • help_text 会在 DRF 的可浏览 API 界面中显示为提示信息,方便 API 使用者理解。
      • 我们已经为自定义的 xxx_namexxx_display 字段设置了 read_only=True,所以它们自然不会在写入时被接受。
      • create_timeupdate_time 通常也应该是只读的,因为它们由 auto_now_addauto_now 自动管理。

更新 ModuleSerializer 以包含项目名称 (可选但推荐)

为了保持一致性,我们也可以在 ModuleSerializer 中添加 project_name 字段,这样在查看模块列表或详情时也能直接看到项目名称。

修改 api/serializers.py 中的 ModuleSerializer

python 复制代码
# test-platform/api/serializers.py

# ... (ProjectSerializer 保持不变) ...

class ModuleSerializer(serializers.ModelSerializer):
    """
    模块序列化器
    """
    # 添加 project_name 字段,使其在序列化输出时包含项目名称
    project_name = serializers.CharField(source='project.name', read_only=True)

    class Meta:
        model = Module
        fields = ['id', 'name', 'description', 'project', 'project_name', 'create_time', 'update_time']
        extra_kwargs = {
            'project': {'write_only': False, 'help_text': "关联的项目ID"},
            'create_time': {'read_only': True},
            'update_time': {'read_only': True},
        }

# ... (TestCaseSerializer 保持不变) ...

现在,当获取模块信息时,响应中也会包含 project_name

第二步:创建 TestCaseViewSet (视图集) - 支持过滤

接下来,创建 TestCaseViewSet。我们将继承 ModelViewSet 并可能添加一些自定义的过滤逻辑。

打开 api/views.py 文件,添加 TestCaseViewSet

python 复制代码
# test-platform/api/views.py

from rest_framework import viewsets
# 如果需要更细致的权限控制,可以导入 permissions
# from rest_framework import permissions
from .models import Project, Module, TestCase
from .serializers import ProjectSerializer, ModuleSerializer, TestCaseSerializer # 导入 TestCaseSerializer

# ... (ProjectViewSet 和 ModuleViewSet 代码保持不变) ...

class TestCaseViewSet(viewsets.ModelViewSet):
    """
    测试用例管理视图集
    提供用例列表、创建、详情、更新、删除等接口
    支持通过查询参数 `module_id` 或 `project_id` 进行过滤
    """
    queryset = TestCase.objects.all() # 默认查询所有测试用例
    serializer_class = TestCaseSerializer
    # permission_classes = [permissions.IsAuthenticated] # 示例:可以添加权限控制,要求用户已登录

    # 自定义 get_queryset 方法以支持动态过滤
    def get_queryset(self):
        """
        重写get_queryset方法,根据请求参数动态过滤查询集
        """
        queryset = super().get_queryset() # 获取基础查询集
        
        # 1. 根据 module_id 过滤
        module_id = self.request.query_params.get('module_id', None)
        if module_id is not None:
            # 确保 module_id 是有效的整数
            try:
                module_id = int(module_id)
                queryset = queryset.filter(module_id=module_id)
            except ValueError:
                # 如果 module_id 不是有效的整数,可以忽略或返回错误
                pass # 或者 raise serializers.ValidationError("module_id 必须是整数")

        # 2. 根据 project_id 过滤 (通过模块关联到项目)
        project_id = self.request.query_params.get('project_id', None)
        if project_id is not None:
            # 确保 project_id 是有效的整数
            try:
                project_id = int(project_id)
                # TestCase -> Module -> Project
                queryset = queryset.filter(module__project_id=project_id)
            except ValueError:
                pass

        return queryset.order_by('-create_time') # 默认按创建时间降序

代码解释:

  1. queryset = TestCase.objects.all(): 默认情况下,视图集将操作所有 TestCase 对象。
  2. serializer_class = TestCaseSerializer: 指定使用我们刚刚创建的 TestCaseSerializer
  3. def get_queryset(self):: 我们重写了 ModelViewSetget_queryset 方法。这个方法在每次需要获取查询集(例如,列表视图或详情视图)时都会被调用。
    • queryset = super().get_queryset(): 首先调用父类的 get_queryset 方法获取基础查询集(即 TestCase.objects.all())。
    • module_id = self.request.query_params.get('module_id', None):
      • self.request 是 DRF 封装的 HTTP 请求对象。
      • query_params 是一个类似字典的对象,包含了 URL 中的查询参数 (例如 ?module_id=1&name=test)。
      • .get('module_id', None) 尝试获取名为 module_id 的查询参数,如果不存在则返回 None
    • if module_id is not None: ... queryset = queryset.filter(module_id=module_id): 如果 module_id 参数存在,就使用 Django ORM 的 filter() 方法来筛选出属于该模块的测试用例。
    • project_id = self.request.query_params.get('project_id', None): 类似地获取 project_id 参数。
    • if project_id is not None: ... queryset = queryset.filter(module__project_id=project_id):
      • 如果 project_id 参数存在,我们使用 module__project_id=project_id 来进行过滤。
      • 这里的 module__project_id 是 Django ORM 的跨关系查询语法,意思是"通过 TestCasemodule 字段,找到关联的 Module 对象,再通过该 Module 对象的 project 字段,找到关联的 Project 对象,并筛选出其 id 等于 project_id 的那些 TestCase"。
    • return queryset.order_by('-create_time'): 最后返回经过筛选和排序的查询集。

通过这种方式,我们的 /api/testcases/ 端点现在可以接受 module_idproject_id 作为查询参数来进行动态过滤。例如:

  • /api/testcases/?module_id=5:获取模块 ID 为 5 的所有测试用例。
  • /api/testcases/?project_id=2:获取项目 ID 为 2 的所有测试用例。
  • /api/testcases/?project_id=2&module_id=5:获取项目 ID 为 2 且模块 ID 为 5 的所有测试用例 (虽然此时 project_id 是冗余的,因为模块已确定项目)。

第三步:注册 TestCaseViewSet 到 Router

现在,我们需要将新的 TestCaseViewSet 添加到我们的 URL 路由中。

打开 api/urls.py 文件,修改如下:

python 复制代码
# test-platform/api/urls.py

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ProjectViewSet, ModuleViewSet, TestCaseViewSet # 导入 TestCaseViewSet

# 创建一个 DefaultRouter 实例
router = DefaultRouter()

# 注册视图集
router.register(r'projects', ProjectViewSet, basename='project')
router.register(r'modules', ModuleViewSet, basename='module')
router.register(r'testcases', TestCaseViewSet, basename='testcase') # 新增 TestCaseViewSet 注册

urlpatterns = [
    path('', include(router.urls)),
]

我们只是在 router.register(...) 中添加了一行来注册 TestCaseViewSet。DRF 的 Router 会自动为它生成所有标准的 CRUD URL。

第四步:测试 TestCase API

万事俱备,只欠测试!

  1. 确保数据库中有一些关联数据:

    • 如果你还没有,请先通过 Django Admin (http://127.0.0.1:8000/admin/) 或之前创建的 Project/Module API (http://127.0.0.1:8000/api/projects/http://127.0.0.1:8000/api/modules/) 创建至少:
      • 一个项目 (例如,Project A, ID=1)
      • 在该项目下的一个模块 (例如,Module X under Project A, ID=1, project=1)
      • 另一个项目 (例如,Project B, ID=2)
      • 在该项目下的一个模块 (例如,Module Y under Project B, ID=2, project=2)
  2. 启动 Django 开发服务器:

    bash 复制代码
    python manage.py runserver
  3. 访问 DRF 的可浏览 API 界面:

    在浏览器中访问 http://127.0.0.1:8000/api/。你能看到新添加的 testcases API 端点。

  4. 测试 TestCase API - GET /api/testcases/ (列表):

    点击 http://127.0.0.1:8000/api/testcases/ 链接。

    • 注意看响应中,会有之前数据,会包含我们定义的 module_name, project_name, priority_display 等字段。
  5. 测试 TestCase API - POST /api/testcases/ (创建):

    http://127.0.0.1:8000/api/testcases/ 页面的底部表单中:

    • 输入名称、描述、选择所属模块等字段,
    • 点击 "POST"。

    如果成功,你会看到返回的创建后的用例数据,其中包含了 module_name, project_name 等。并且列表会刷新。

  6. 测试 TestCase API - GET /api/testcases/?module_id={id} (按模块过滤):

    • 假设你创建的 Module X 的 ID 是 1。在浏览器地址栏输入 http://127.0.0.1:8000/api/testcases/?module_id=1 并回车。
    • 你只能看到属于 Module 1 的测试用例。
  7. 测试 TestCase API - GET /api/testcases/?project_id={id} (按项目过滤):

    • 假设你创建的 Project A 的 ID 是 1。在浏览器地址栏输入 http://127.0.0.1:8000/api/testcases/?project_id=1 并回车。
    • 你只能看到属于 Project 1 下所有模块的测试用例。
  8. 测试其他操作:

    • GET /api/testcases/{id}/ (详情): 点击列表中的某个用例链接。
    • PUT /api/testcases/{id}/ (更新): 在详情页面修改数据并提交。
    • PATCH /api/testcases/{id}/ (部分更新): 类似 PUT,但只提供需要修改的字段。
    • DELETE /api/testcases/{id}/ (删除): 在详情页面点击删除按钮。

通过这些测试,你能验证 TestCase API 的所有功能,包括关联数据显示和动态过滤。

总结

在这篇文章中,我们成功地为 TestCase 模型构建了功能更完善的 API 接口:

  • ✅ 增强了 TestCaseSerializer,使其能够:
    • 通过 source 参数和模型方法 (get_FIELD_display) 显示关联对象的名称和 choices 字段的可读描述。
    • 明确了哪些字段是只读的,哪些是可写的。
  • ✅ 更新了 ModuleSerializer 以包含 project_name
  • ✅ 创建了 TestCaseViewSet,并重写了 get_queryset 方法,以支持根据 module_idproject_id URL 查询参数进行动态过滤。
  • ✅ 将 TestCaseViewSet 注册到了 DRF Router 中。
  • ✅ 通过 DRF 的可浏览 API 界面全面测试了 TestCase API 的创建、读取(包括过滤)、更新和删除功能。

至此,我们测试平台的后端核心数据(项目、模块、测试用例)的 CRUD API 已经基本完成!这些 API 将为我们接下来的前端开发提供坚实的数据基础。

相关推荐
码界奇点6 小时前
Python从0到100一站式学习路线图与实战指南
开发语言·python·学习·青少年编程·贴图
Laravel技术社区7 小时前
pytesseract 中英文 识别图片文字
python
生骨大头菜8 小时前
使用python实现相似图片搜索功能,并接入springcloud
开发语言·python·spring cloud·微服务
绝不收费—免费看不了了联系我8 小时前
Fastapi的单进程响应问题 和 解决方法
开发语言·后端·python·fastapi
xqqxqxxq8 小时前
背单词软件技术笔记(V2.0扩展版)
java·笔记·python
最晚的py9 小时前
Python抓取ZLibrary元数据
爬虫·python
咖啡续命又一天9 小时前
Trae CN IDE 中 Python 开发的具体流程和配置总结
开发语言·ide·python·ai编程
IT·小灰灰10 小时前
告别“翻墙“烦恼:DMXAPI让Gemini-3-pro-thinking调用快如闪电
网络·人工智能·python·深度学习·云计算
山海青风10 小时前
语音合成 - 用 Python 合成藏语三大方言语音
开发语言·python·音视频
mikejahn10 小时前
爬取CECS网站征求意见栏目的最新信息
python