在后端开发中,自己写测试样例还是非常重要的,不然每次修改程序以后手动测试,工作量又大,还很难测完整。

Django runserver 时测试 API

DRF 教程提到,在 runserver 时手动测试看效果时可以使用 httpie 或者其他工具。但是 POST 数据似乎有点麻烦。博主更常使用 Python 的 requests 库。

1
pip install requests
1
2
3
4
5
from requests import get, post, put, delete
post("http://localhost:8000/login/", {
"username": "lyh543",
"password": "password"
})

语法和 django.test.client.Client 几乎一模一样。

Django test 邮件服务

Django test 还会替换掉默认的 SMTP 服务器,改为一个虚拟的、不会真正发送邮件的服务器。

文档:https://docs.djangoproject.com/zh-hans/3.1/topics/testing/tools/#email-services

官方也给了一个读取发件箱的方法,这样每次测试的时候就不用人工查询邮件,而是直接在测试代码里读取邮件信息,再配合正则表达式就可以提取出需要的信息了。下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
from django.core import mail

def pop_token_from_virtual_mailbox(test_function):
"""
测试时从虚拟的邮箱中找到验证码,并清空测试发件箱
虚拟邮箱:https://docs.djangoproject.com/zh-hans/3.1/topics/testing/tools/#email-services
调用示例:https://github.com/uestc-msc/uestcmsc_webapp_backend/blob/5ca6316e6de8c42f28e3b7e9f0866b5cba4280c8/users/tests.py#L188
"""
test_function.assertEqual(len(mail.outbox), 1)
message = mail.outbox[0].message().as_string()
mail.outbox = []
token = re.findall('token=.+', message)[0][6:]
return token

上面这个函数自动抓取发送邮件中的 token=XXXXX 字段中的 XXXXX,保存到 token 变量然后返回。

Django test “修改当前时间”

博主在写签到的 TestCase 的时候,想要修改 now() 时间来进行测试。Google 了一下找到了 mock 的几种写法,这里演示一种(源代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from unittest import mock

class ActivityCheckInTest(TestCase):
@mock.patch('activities.views.now')
def test_check_in_anytime(self, mocked_now): # 需要在 test 函数的参数部分增加一个参数
for day in [1, 2, 3]:
is_today = day == 2
for hour in range(24):
mocked_now.return_value = datetime(2020, 1, day, hour, 15, tzinfo=pytz.timezone('Asia/Shanghai')) # 可任意修改 activities.views.now 的参数
client = Client()
client.force_login(self.user)
response = client.post(activity_check_in_url(self.activity.id), { # 此处测试的 activities.views.now 会返回上面的 return_value
"check_in_code": self.activity.check_in_code
})
self.assertEqual(response.status_code, 200 if is_today else 403, f'date={mocked_now.return_value}')
self.activity.refresh_from_db()
self.assertEqual(self.activity.attender.count(), 1 if is_today else 0)
self.activity.attender.clear()

Django test 和 Integration Error?

博主在写登录的 TestCase 时出现了很奇怪的现象:正常运行时 API 貌似没有问题,在一个 Test 函数中调用一次 login 函数也没有问题,但如果调用两次 login 函数,Python
解释器会不报错而停止,错误码为 -1073741819 (0xC0000005)login() 函数如下:

1
2
3
4
5
6
7
8
9
10
def login(request):
try:
username = request.data['username']
password = request.data['password']
with transaction.atomic():
user = authenticate(request, username=username, password=password)
django_login(request, user)
return Response(status=status.HTTP_200_OK)
except IntegrityError or KeyError:
return Response(status=status.HTTP_401_UNAUTHORIZED)

test 函数如下:

1
2
3
4
5
6
7
class LogInTest(TestCase):
def test_log_in_with_less_argument(self):
r = Client().post('/users/login/')
self.assertEqual(r.status_code, 401)

r = Client().post('/users/login/')
self.assertEqual(r.status_code, 401)

我参考了 Django 文档的 事务 部分,按照官方推荐的方法编写这段代码,但是出了问题。

个人猜测可能是 TestCase 中涉及的数据库回滚和 IntegrityError 触发回滚的冲突?

最终我只能按照 if 的方法替代掉 try-catch 的方法。尽量不要触发 IntegrityError 吧。

Django test 时,POST 和 PATCH 记得添加 content_type=’application/json’

笔者已经两次被这个坑了。第一次是在测试 PATCH 时,使用 django.test.client.Client.patch(path, data),返回的 HTTP 状态码为 415 Unsupported media type "application/octet-stream" in request.'

添加参数 Client.patch(path, data, content_type='application/json') 就好了。

Getting 415 code with django.test.Client’s patch method

后来,在 POST 的时候莫名其妙发现我写的下面这段 JSON,手动 POST 时能正常工作,但使用 Client.post(path, data) 时,嵌套的 {"id":1} 部分不能被正确识别到。

1
2
3
4
5
6
{
"title": "test",
"datetime": "2021-01-20T10:29:26+08:00",
"location": "test",
"presenter": [{"id":1}]
}

DEBUG 的时候注意到,response 中包含的 wsgi_request 里面,{"id":1} 就没有被正确提交。猜测可能是 Django Client 没有以 JSON 的形式解析这段代码,于是加上 content_type='application/json',就返回 201 Created 了。