Hello,
I am currently writing test cases using Django and would like to improve the way we handle errors, particularly distinguishing between errors that arise from the test cases themselves and those caused by the user’s code.
Context:
Let’s say a user writes a function to fetch and increment the likes count of a post:
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")
Here’s the test case for that function:
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)
Scenario:
Now, if a user accidentally modifies the code to increment the likes by 2 instead of 1 and didn’t save the post object.
# 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")
This leads to the following test failure:
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 ===============================
Request for Assistance:
Instead of displaying the above traceback, which clearly indicates a test case error, I would prefer to return a more user-friendly error message like:
Error 1: "Likes cannot be incremented by 2"
Error 2: "You used filter method instead of the get method in your function"
Is there a way to catch such errors within the test case and return a more human-readable error message? Any guidance on how to implement this would be greatly appreciated.
Thank you!
Edit 1
Note: I am using a celery worker to run the dockerfile that will in turn run the tests. Here is the relevant part of the tasks.py responsible for printing error:
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())