Обработка ошибок в тестовых примерах Django

В настоящее время я пишу тестовые примеры, используя 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"

Есть ли способ отлавливать такие ошибки внутри тестового примера и возвращать более понятное для человека сообщение об ошибке? Любое руководство по реализации этого было бы очень признательно.

Спасибо!


Редактирование 1

Примечание: я использую 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 не бесплатна, ее делает команда (платных) разработчиков.

Вместо того чтобы отобразить приведенный выше traceback, который явно указывает на ошибку тестового случая

Это не ошибка тест-кейса: независимо от того, как вы запускаете представление, оно всегда будет ошибаться, просто потому что типы не совпадают.

Некоторые инструменты, такие как программы проверки типов, уже могут находить ошибки, выполняя статический анализ, и поэтому даже не требуют запуска функции, они могут определить, что Post.objects.filter(id=post_id) является QuerySet[Post] объектом, и такой QuerySet не имеет likes атрибута. Смотрите, например, этот блогпост.

Но я не вижу причин, по которым человекочитаемое сообщение об ошибке могло бы сильно помочь. Как уже говорилось, для этого нужно отследить всевозможные ошибки. В лучшем случае Sentry может предложить решения для некоторых (очень) распространенных проблем. Большинство проблем более специфичны, и поэтому Sentry не сможет сильно помочь. Преобразование в человекочитаемый текст, вероятно, требует обработки естественного языка, что, безусловно, нелегко настроить, но, что более важно, трассировка дает очень полезную обратную связь для программиста.

Вернуться на верх