Django使用心得(二) 使用TestCase测试接口

栏目: Python · 发布时间: 6年前

内容简介:在接触开源社区在目前的公司面试了一些程序员,他们的工作经验平均都有三年以上,但是都没有编写单元测试的习惯。 问到

Django使用心得(二) 使用TestCase测试接口

在接触开源社区 Github 之后,发现特别多的开源项目都会有单元测试 TestCase 。但是在步入工作后,从业了两个创业公司,发现大多数 程序员 都没有养成写单元测试的习惯。

在目前的公司面试了一些程序员,他们的工作经验平均都有三年以上,但是都没有编写单元测试的习惯。 问到 "为什么不去编写单元测试呢?" ,无非就是回答 "没有时间""写的都是接口,直接用客户端 工具 测试一下就可以了"

在笔者使用了 Django 框架自带的 TestCase 之后,发现用 TestCase 测试接口不仅比一些 客户端工具 方便,而且还能降低在对代码进行修改之后出现 BUG 的几率, 特别是一些对代码有严重的洁癖喜欢优化代码的程序员来说真的非常有用。

而且运用框架的 TestCase 编写单元测试,还能结合一些 CI 工具来实现自动化测试,这个我也会专门写一篇文章来介绍我利用 Gitlab CI 结合 DjangoTestCase 实现自动化测试的一些心得。

TestCase 类的结构

为了方便没用用过 TestCase 的读者,先简单介绍一下 TestCase 的类结构。

常见的 TestCasesetUp 函数、 tearDown 函数和 test_func 组成。

这里 test_func 是指你编写了测试逻辑的函数,而 setUp 函数则是在 test_func 函数之前执行的函数, tearDown 函数则是在 test_func 执行之后执行的函数。

This Github Sample is by elfgzp

development_of_test_habits/tests/test_demo.py view raw

from django.test import TestCase


class Demo(TestCase):
    def setUp(self):
        print('setUp')

    def tearDown(self):
        print('tearDown')

    def test_demo(self):
        print('test_demo')

    def test_demo_2(self):
        print('test_demo2')复制代码

我们可以通过在 Django 项目的根目录运行以下命令来运行这个单元测试

python manage.py test development_of_test_habits.tests.test_demo.Demo
复制代码

如果使用 Pycharm 来运行的话可以直接点击类左侧的运行箭头,更加方便地运行或者 Debug 这个单元测试。

Django使用心得(二) 使用TestCase测试接口

可以从运行后的结果清晰的看到这个单元测试的执行顺序。

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
setUp
test_demo
tearDown
.setUp
test_demo2
tearDown
.
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK
Destroying test database for alias 'default'...
复制代码

此外还可以从运行结果看到,在测试之前单元测试创建了一个测试数据库。

Creating test database for alias ‘default’…

然后在测试结束将数据库摧毁。

Destroying test database for alias ‘default’…

这个也就是在继承了 Django 框架中的 TestCase ,它已经帮你实现的一些逻辑方便用于测试,所以我们不需要在 setUptearDown 函数中实现这些逻辑。

利用TestCase测试接口

接下来讲一下我们如何使用 TestCase 来测试接口的,首先我们编写一个简单的接口,这里笔者是用 Django Rest FrameworkAPIView 来编写的,读者也可以使用自己管用的方法来编写。

This Github Sample is by elfgzp

development_of_test_habits/views/hello_test_case.py view raw

from rest_framework.views import APIView
from rest_framework.response import Response


class HelloTestCase(APIView):
    def get(self, request, *args, **kwargs):
        return Response({
            'msg': 'Hello %s I am a test Case' % request.query_params.get('name', ',')
        })复制代码

然后这个接口类加到我们的路由中。

This Github Sample is by elfgzp

development_of_test_habits/urls.py view raw

from django.urls import path
from development_of_test_habits import views

urlpatterns = [
    path('hello_test_case', views.HelloTestCase.as_view(), name='hello_test_case'),
]复制代码

接下来我们编写一个 HelloTestCase 的单元测试类来测试我们的测试用例。

This Github Sample is by elfgzp

development_of_test_habits/tests/test_hello_test_case.py view raw

from django.urls import resolve, reverse
from django.test import TestCase


class HelloTestCase(TestCase):
    def setUp(self):
        self.name = 'Django'

    def test_hello_test_case(self):
        url = '/test_case/hello_test_case'
        # url = reverse('hello_test_case')
        # Input: print(resolve(url))
        # Output: ResolverMatch(func=development_of_test_habits.views.hello_test_case.HelloTestCase, args=(), kwargs={}, url_name=hello_test_case, app_names=[], namespaces=[])
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)  # 期望的Http相应码为200
        data = response.json()
        self.assertEqual(data['msg'], 'Hello , I am a test Case')  # 期望的msg返回结果为'Hello , I am a test Case'

        response = self.client.get(url, {'name': self.name})
        self.assertEqual(response.status_code, 200)  # 期望的Http相应码为200
        data = response.json()
        self.assertEqual(data['msg'], 'Hello Django I am a test Case')  # 期望的msg返回结果为'Hello Django I am a test Case'复制代码

setUp 函数中,我定义了一个 name 属性并且赋值为 Django 便于后面使用。

单元测试测试接口主要分为下面几个重要的内容。

请求的路由地址

在测试接口时无非就是发起请求,检查返回的状态嘛和响应内容是否正确。发请求肯定少不了 url 地址,这里有两种方式来配置请求地址。

1.直接设置请求地址

url = '/test_case/hello_test_case'
复制代码

2.透过 django.urls.reverse 函数和在路由设置的 name 来得到请求的地址

url =  reverse('hello_test_case')
复制代码

这里在介绍以下我们还可以通过 django.urls.resolveurl 得到对应的接口类或者接口函数`。

请求的客户端

发起请求我们除了需要路由外,我们还需要一个发起请求的客户端。python的 requests 库就是很好的客户端工具,只不过 Django 在它的 TestCase 类 中已经集成了一个客户端工具,我们只需要调用 TestCaseclient 属性就可以得到一个客户端。

client = self.client
复制代码

发起请求

发起请求非常简单只需要一行代码,我们就可以通过请求得到它的响应体。

response = self.client.get(url)
复制代码

如果需要携带参数只需要传入 data 参数。

response = self.client.get(url, {'name': self.name})
复制代码

验证响应体

在单元测试中, TestCaseassertEqual 有点类似 pythonassert 函数,除了 assertEqual 外还有 assertNotEqualassertGreaterassertIn 等等。 这里笔者主要做了两个检查,一个是检查 status_code 是否等于 200

self.assertEqual(response.status_code, 200)  # 期望的Http相应码为200
复制代码

另一个是检查响应内容是否正确。

data = response.json()
self.assertEqual(data['msg'], 'Hello , I am a test Case')  # 期望的msg返回结果为'Hello , I am a test Case'
复制代码

这个就是最简单的测试请求的单元测试,但是在实际的接口中,我们是需要数据的,所以我们还需要生成测试数据。

这里介绍一个非常方便的库 mixer ,可以方便在我们的单元测试中生成测试数据。

利用mixer在TestCase中生成测试数据

首先我们定一个场景,比如说我们记录了学校班级的学生的作业,需要一个接口来返回学生的作业列表,并且这个接口是需要用户登陆后才可以请求的,定义的 models 和接口类如下。

This Github Sample is by elfgzp

development_of_test_habits/models.py view raw

from django.db import models


class School(models.Model):
    name = models.CharField(max_length=32)


class Class(models.Model):
    school_id = models.ForeignKey(to=School, on_delete=models.PROTECT)
    name = models.CharField(max_length=32)


class Student(models.Model):
    class_id = models.ForeignKey(to=Class, on_delete=models.PROTECT)
    name = models.CharField(max_length=32)


class HomeWork(models.Model):
    student_id = models.ForeignKey(to=Student, on_delete=Student)
    name = models.CharField(max_length=32)复制代码

接口笔者用的是 Django rest frameworkReadOnlyModelViewSet 视图类实现的,实现的功能就是返回一个 json 结果集, 并且 json 中有 HomeWorkSchool NameClass NameStudent Name ,视图类代码和序列化代码如下。

This Github Sample is by elfgzp

development_of_test_habits/views/api/home_work.py view raw

from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.permissions import IsAuthenticated

from development_of_test_habits.models import HomeWork
from development_of_test_habits.serializers import HomeWorkSerializer


class HomeWorkViewSet(ReadOnlyModelViewSet):
    queryset = HomeWork.objects.all()
    serializer_class = HomeWorkSerializer
    permission_classes = (IsAuthenticated, )复制代码

This Github Sample is by elfgzp

development_of_test_habits/serializers.py view raw

from rest_framework import serializers

from development_of_test_habits.models import HomeWork


class HomeWorkSerializer(serializers.ModelSerializer):
    class Meta:
        model = HomeWork
        fields = ('school_name', 'class_name', 'student_name', 'name')

    school_name = serializers.CharField(source='student_id.class_id.school_id.name', read_only=True)
    class_name = serializers.CharField(source='student_id.class_id.name', read_only=True)
    student_name = serializers.CharField(source='student_id.name', read_only=True)复制代码

最后把我们的接口类添加到路由中。

This Github Sample is by elfgzp

development_of_test_habits/serializers.py view raw

urlpatterns = [
    path('hello_test_case', views.HelloTestCase.as_view(), name='hello_test_case'),
    path('api/home_works', views.HomeWorkViewSet.as_view({'get': 'list'}), name='home_works_list')
]复制代码

完成接口的编写,可以开始写单元测试了,定义 HomeWorkAPITestCase 测试类并且在 setUp 中生成测试数据。

This Github Sample is by elfgzp

development_of_test_habits/tests/test_home_works_api.py view raw

from django.test import TestCase
from django.urls import reverse

from django.contrib.auth.models import User

from mixer.backend.django import mixer

from development_of_test_habits import models


class HomeWorkAPITestCase(TestCase):
    def setUp(self):
        self.user = mixer.blend(User)

        self.random_home_works = [
            mixer.blend(models.HomeWork)
            for _ in range(11)
        ]复制代码

这里介绍一下 mixer 这个模块,这个模块会根据你定义的模型和模型的字段来随机生成测试数据, 包括这个数据的外键数据 。 这样在我们这种层级非常多的关系型数据就非常的方便,否则需要一层一层的去生成数据。 代码中就利用 mixer 生成了一个随机的用户和11个随机的 HomeWork 数据。

接下来编写测试的逻辑代码。

This Github Sample is by elfgzp

development_of_test_habits/tests/test_home_works_api.py view raw

class HomeWorkAPITestCase(TestCase):
    def setUp(self):
        self.user = mixer.blend(User)

        self.random_home_works = [
            mixer.blend(models.HomeWork)
            for _ in range(11)
        ]

    def test_home_works_list_api(self):
        url = reverse('home_works_list')

        response = self.client.get(url)
        self.assertEqual(response.status_code, 403)

        self.client.force_login(self.user)
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        data = response.json()
        self.assertEqual(len(data), len(self.random_home_works))

        data_fields = [key for key in data[0].keys()]

        self.assertIn('school_name', data_fields)
        self.assertIn('class_name', data_fields)
        self.assertIn('student_name', data_fields)
        self.assertIn('name', data_fields)复制代码

首先通过 django.urls.reverse 函数和接口的路由名称获得 url ,第一步先测试用户在没有登陆的情况下请求接口,这里期望的请求响应码为 403

response = self.client.get(url)
self.assertEqual(response.status_code, 403)
复制代码

我们通过 client 的一个 登陆 函数 force_login 来登陆我们随机生成的用户,再次请求接口,这次的期望的请求相应码就为 200

self.client.force_login(self.user)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
复制代码

最后验证返回的结果数量是和结果中定义的字段是否正确。

data = response.json()
self.assertEqual(len(data), len(self.random_home_works))

data_fields = [key for key in data[0].keys()]

self.assertIn('school_name', data_fields)
self.assertIn('class_name', data_fields)
self.assertIn('student_name', data_fields)
self.assertIn('name', data_fields)
复制代码

以上就是在项目中测试接口的最常见的流程。

TestCase在使用中需要注意的一些问题

假设我们要在接口中增加 请求头 ,以 HelloTestCase 接口为例,我们要增加一个 TEST_HEADER 的请求头,则在接口的逻辑处理中,就需要给这个请求头 加上 HTTP_ 前缀。

This Github Sample is by elfgzp

development_of_test_habits/views/hello_test_case.py view raw

class HelloTestCase(APIView):
def get(self, request, *args, **kwargs):
    data = {
        'msg': 'Hello %s I am a test Case' % request.query_params.get('name', ',')
    }
    test_header = request.META.get('HTTP_TEST_HEADER')
    if test_header:
        data['test_header'] = test_header
    return Response(data)复制代码

如果我们用客户端工具类似 Post ManRestFul Client 等等,请求时只要在请求头中加上 TEST_HEADER 即可。 但是在单元测试中,我们也需要把 HTTP_ 这个前缀加上,否则接口逻辑是无法获取的。

This Github Sample is by elfgzp

development_of_test_habits/tests/test_hello_test_case.py view raw

def test_hello_test_case(self):
    url = '/test_case/hello_test_case'
    # url = reverse('hello_test_case')
    # Input: print(resolve(url))
    # Output: ResolverMatch(func=development_of_test_habits.views.hello_test_case.HelloTestCase, args=(), kwargs={}, url_name=hello_test_case, app_names=[], namespaces=[])
    response = self.client.get(url)
    self.assertEqual(response.status_code, 200)  # 期望的Http相应码为200
    data = response.json()
    self.assertEqual(data['msg'], 'Hello , I am a test Case')  # 期望的msg返回结果为'Hello , I am a test Case'

    response = self.client.get(url, {'name': self.name})
    self.assertEqual(response.status_code, 200)  # 期望的Http相应码为200
    data = response.json()
    self.assertEqual(data['msg'], 'Hello Django I am a test Case')  # 期望的msg返回结果为'Hello Django I am a test Case'

    # 假设我们要在接口中增加请求头'TEST_HEADER'
    # 则在测试时需要加上前缀'HTTP_'最终的结果为'HTTP_TEST_HEADER'
    response = self.client.get(url, **{'HTTP_TEST_HEADER': 'This is a test header.'})
    data = response.json()
    self.assertEqual(data['test_header'], 'This is a test header.')复制代码

总结

在用测试用例来测试接口后,笔者已经开始养成写完接口直接用单元测试来测试的习惯,这样不单是在给别人说明自己的接口的功能,还是减少线上环境的BUG都有明显的帮助, 希望读者也能用这种方式不断的养成写单元测试的好习惯。

本人博客原文地址:elfgzp.cn/2018/12/07/…


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Python灰帽子

Python灰帽子

[美] Justin Seitz / 丁赟卿 译、崔孝晨 审校 / 电子工业出版社 / 2011-3 / 39.00元

《Python灰帽子》是由知名安全机构Immunity Inc的资深黑帽Justin Seitz主笔撰写的一本关于编程语言Python如何被广泛应用于黑客与逆向工程领域的书籍。老牌黑客,同时也是Immunity Inc的创始人兼首席技术执行官(CTO)Dave Aitel为这本书担任了技术编辑一职。书中绝大部分篇幅着眼于黑客技术领域中的两大经久不衰的话题:逆向工程与漏洞挖掘,并向读者呈现了几乎每个......一起来看看 《Python灰帽子》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具