我目前正在使用 Django 编写测试用例,并希望改进我们处理错误的方式,特别是区分测试用例本身引起的错误和用户代码引起的错误。
假设用户编写了一个函数来获取并增加帖子的点赞数:
def fetch_post(request, post_id):
try:
post = Post.objects.get(id=post_id)
except Post.DoesNotExist:
raise Http404("Post not found")
post.likes += 1
post.save()
return HttpResponse("Post liked")
这是该函数的测试用例:
from django.test import TestCase
from project_app.models import Post
from django.urls import reverse
from django.http import Http404
class FetchPostViewTests(TestCase):
def setUp(self):
self.post = Post.objects.create(title="Sample Post")
def assertLikesIncrementedByOne(self, initial_likes, updated_post):
if updated_post.likes != initial_likes + 1:
raise AssertionError(f'Error: "Likes cannot be incremented by {updated_post.likes - initial_likes}"')
def test_fetch_post_increments_likes(self):
initial_likes = self.post.likes
response = self.client.get(reverse('fetch_post', args=[self.post.id]))
updated_post = Post.objects.get(id=self.post.id)
self.assertLikesIncrementedByOne(initial_likes, updated_post)
self.assertEqual(response.content.decode(), "Post liked")
def test_fetch_post_not_found(self):
response = self.client.get(reverse('fetch_post', args=[9999]))
self.assertEqual(response.status_code, 404)
现在,如果用户不小心修改了代码,将点赞数增加了 2 而不是 1,并且没有保存帖子对象。
# Wrong code that will fail the test case
def fetch_post(request, post_id):
try:
# used filter() method instead of get() method
post = Post.objects.filter(id=post_id)
except Post.DoesNotExist:
raise Http404("Post not found")
post.likes += 2 # incremented by two instead of 1
post.save()
return HttpResponse("Post liked")
这会导致以下测试失败:
test_cases/test_case.py:53:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/usr/local/lib/python3.9/site-packages/django/test/client.py:742: in get
response = super().get(path, data=data, secure=secure, **extra)
/usr/local/lib/python3.9/site-packages/django/test/client.py:396: in get
return self.generic('GET', path, secure=secure, **{
/usr/local/lib/python3.9/site-packages/django/test/client.py:473: in generic
return self.request(**r)
/usr/local/lib/python3.9/site-packages/django/test/client.py:719: in request
self.check_exception(response)
/usr/local/lib/python3.9/site-packages/django/test/client.py:580: in check_exception
raise exc_value
/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py:47: in inner
response = get_response(request)
/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py:181: in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
request = <WSGIRequest: GET '/fetch_post/9999/'>, post_id = 9999
def fetch_post(request, post_id):
try:
post = Post.objects.filter(id=post_id)
except Post.DoesNotExist:
raise Http404("Post not found") # Raise 404 if post does not exist
> post.likes += 2
E AttributeError: 'QuerySet' object has no attribute 'likes'
project_app/views.py:11: AttributeError
------------------------------ Captured log call -------------------------------
ERROR django.request:log.py:224 Internal Server Error: /fetch_post/9999/
Traceback (most recent call last):
File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/app/project_app/views.py", line 11, in fetch_post
post.likes += 2
AttributeError: 'QuerySet' object has no attribute 'likes'
=========================== short test summary info ============================
FAILED test_cases/test_case.py::FetchPostViewTests::test_fetch_post_increments_likes
FAILED test_cases/test_case.py::FetchPostViewTests::test_fetch_post_not_found
============================== 2 failed in 0.74s ===============================
我不想显示上面的回溯(它清楚地表明测试用例错误),而是更愿意返回更用户友好的错误消息,例如:
Error 1: "Likes cannot be incremented by 2"
Error 2: "You used filter method instead of the get method in your function"
有没有办法在测试用例中捕获此类错误并返回更易于理解的错误消息?任何有关如何实施这一点的指导将不胜感激。
谢谢!
注意:我正在使用 celery Worker 来运行 dockerfile,该文件将依次运行测试。这是tasks.py中负责打印错误的相关部分:
print("Running tests in Docker container...")
test_result = subprocess.run(
["docker", "run", "--rm", "-v", f"{volume_name}:/app", "test-runner"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
if test_result.returncode != 0:
# Check for AssertionError messages in stdout
error_message = test_result.stdout
if "AssertionError" in error_message:
# Extract just the assertion error message
lines = error_message.splitlines()
for line in lines:
if "Error:" in line:
submission.error_log = line.strip() # Save specific error message to the database
raise Exception(line.strip())
有没有办法在测试用例中捕获此类错误并返回更易于理解的错误消息?任何有关如何实施这一点的指导将不胜感激。
像Sentry这样的工具尝试根据错误的“指纹”找到解决方案。那么这样的工具是否存在,在某种程度上,是的。这些有时甚至可以直接提出修复方法。
但正如您可能理解的那样,这需要经历各种场景,找到共同原因,记录该原因并提出解决方案。这需要大量工作,因此Sentry不是免费的,它是由(付费)开发人员团队制作的。
而不是显示上面的回溯,它清楚地表明测试用例错误
这不是测试用例错误:无论您如何触发视图,它总是会出错,只是因为类型不匹配。
像类型检查器这样的一些工具已经可以通过执行static分析来发现错误,因此甚至不需要运行函数,这些工具可以确定
Post.objects.filter(id=post_id)
是一个QuerySet[Post]
对象,并且这样的QuerySet
没有likes
属性。例如,请参阅此博文。
但我不认为人类可读的错误消息会有多大帮助。如前所述,它需要对各种错误进行指纹识别。 Sentry 充其量可以为一些(非常)常见的问题提供解决方案。大多数问题都比较具体,因此 Sentry 无法提供太多帮助。将其转换为人类可读的文本可能需要一些自然语言处理,这绝对不容易设置,但更重要的是,回溯为程序员提供了非常有用的反馈。