Django TestCase tests for CreateView, UpdateView, DeleteView return unexpected result

I’m implementing some tests for my views. My app is forum app, with structure “forum->subforum->topic->comments”. I chose views of topics and comments for testing. Tests are created with the help of factory_boy library.

As far as I’ve understood, the real DB isn’t touched during the testing, for these means some test DB is created. Somehow this test DB isn’t affected by my manipulations.

factories.py:
(In UserFactory I’ve tried to implement some kind of “choices” from already created users in my DB, but it resulted in a failure; so I commented them, getting back to generic variants, and inserted self.client.force_login(self.user) into test’s setUp method)

import factory

from django.contrib.auth.models import User

from forum import models


class UserFactory(factory.django.DjangoModelFactory):
    username = factory.Faker('user_name')
    password = factory.Faker('password')
    email = factory.Faker('email')
    '''
    username = factory.Faker('random_element', elements=[u for u in models.User.objects.all().values_list('username', flat=True).order_by('id')])
    password = factory.Faker('random_element', elements=[p for p in models.User.objects.all().values_list('password', flat=True).order_by('id')])
    email = factory.Faker('random_element', elements=[e for e in models.User.objects.all().values_list('email', flat=True).order_by('id')])
    '''

    class Meta:
        model = User


class SubForumFactory(factory.django.DjangoModelFactory):
    title = factory.Faker('random_element', elements=['News',
                                                      'Characters',
                                                      'Episodes',
                                                      'References',
                                                      'Production',
                                                      'Bugs'])
    slug = factory.Faker('random_element', elements=['news',
                                                     'characters',
                                                     'episodes',
                                                     'references',
                                                     'production',
                                                     'bugs'])

    class Meta:
        model = models.Subforum


class TopicFactory(factory.django.DjangoModelFactory):
    subject = factory.Faker('sentence')
    slug = factory.Faker('slug')
    first_comment = factory.Faker('text')
    created = factory.Faker('date')
    creator = factory.SubFactory(UserFactory)
    subforum = factory.SubFactory(SubForumFactory)

    class Meta:
        model = models.Topic


class CommentFactory(factory.django.DjangoModelFactory):
    #subforum = factory.SubFactory(SubForumFactory)
    topic = factory.SubFactory(TopicFactory)
    author = factory.SubFactory(UserFactory)
    content = factory.Faker('text')
    created = factory.Faker('date')
    pk = factory.Faker('pyint')

    class Meta:
        model = models.Comment

views.py:

class SubForumListView(ListView):
    model = Subforum
    context_object_name = 'subforum_list'
    template_name = "forum/forum.html"

    def get_context_data(self, **kwargs):
        subforums = Subforum.objects.all()
        context = {'subforums': subforums}
        return context


class TopicListView(FilterView):
    model = Topic
    template_name = "forum/subforum.html"
    slug_url_kwarg = 'subforum_slug'
    context_object_name = 'topics'
    filterset_class = TopicFilter

    def get_queryset(self):
        qs = self.model.objects.all()
        if self.kwargs.get('subforum_slug'):
            qs = qs.filter(subforum__slug=self.kwargs['subforum_slug'])
        return qs


class ShowTopic(DetailView):
    model = Topic
    template_name = "forum/topic.html"
    slug_url_kwarg = 'topic_slug'
    context_object_name = 'topic'

    def get_context_data(self, **kwargs):
        topic = get_object_or_404(Topic, slug=self.kwargs['topic_slug'])
        comments = Comment.objects.filter(topic=topic)
        comments_number = len(Comment.objects.filter(topic__id=topic.id))
        context = {'menu': menu,
                   'topic': topic,
                   'comments': comments,
                   'comm_num': comments_number}
        return context


class AddTopic(DataMixin, CreateView):
    form_class = AddTopicForm
    template_name = 'forum/addtopic.html'
    page_title = 'Create a topic'

    def get_success_url(self):
        return reverse('forum:topic', kwargs={
            'subforum_slug': self.kwargs['subforum_slug']})

    def form_valid(self, form):
        subforum = Subforum.objects.get(slug=self.kwargs['subforum_slug'])
        form.instance.creator = self.request.user
        form.instance.subforum = subforum
        return super(AddTopic, self).form_valid(form)


class AddComment(LoginRequiredMixin, DataMixin, CreateView):
    model = Comment
    form_class = AddCommentForm
    template_name = 'forum/addcomment.html'
    page_title = 'Leave a comment'

    def get_success_url(self):
        return reverse('forum:topic', kwargs={
            'subforum_slug': self.kwargs['subforum_slug'],
            'topic_slug': self.kwargs['topic_slug']})

    def form_valid(self, form):
        topic = Topic.objects.get(slug=self.kwargs['topic_slug'])
        form.instance.author = self.request.user
        form.instance.topic = topic
        return super(AddComment, self).form_valid(form)


class UpdateComment(LoginRequiredMixin, DataMixin, UpdateView):
    model = Comment
    form_class = AddCommentForm
    context_object_name = 'comment'
    template_name = 'forum/editcomment.html'
    page_title = 'Edit comment'

    def get_success_url(self):
        return reverse('forum:topic', kwargs={
            'subforum_slug': self.kwargs['subforum_slug'],
            'topic_slug': self.kwargs['topic_slug']
        })

    def form_valid(self, form):
        topic = Topic.objects.get(slug=self.kwargs['topic_slug'])
        comment_id = self.kwargs['pk']
        form.instance.author = self.request.user
        form.instance.topic = topic
        form.instance.id = comment_id
        return super(UpdateComment, self).form_valid(form)


class DeleteComment(LoginRequiredMixin, DataMixin, DeleteView):
    model = Comment
    context_object_name = 'comment'
    template_name = 'forum/comment_confirm_delete.html'
    page_title = "Delete commen"
    fields = '__all__'

    def get_success_url(self):
        return reverse('forum:topic', kwargs={
            'subforum_slug': self.kwargs['subforum_slug'],
            'topic_slug': self.kwargs['topic_slug']
        })

forms.py:

class AddTopicForm(forms.ModelForm):
    subject = forms.CharField(label="Заголовок", max_length=100, min_length=7)
    first_comment = forms.CharField(label="Сообщение", widget=forms.Textarea())

    class Meta:
        model = models.Topic
        fields = ['subject', 'first_comment']

    def clean_subject(self):
        subject = self.cleaned_data['subject']
        if len(subject) > 100:
            raise ValidationError("Too long, more than 100 symbols")
        if len(subject) < 7:
            raise ValidationError("Too short, no less than 7 symbols required")
        return subject


class AddCommentForm(forms.ModelForm):
    content = forms.CharField(label="Comment text", max_length=2000, min_length=1, widget=forms.Textarea())

    class Meta:
        model = models.Comment
        fields = ['content']

urls.py:

from django.urls import path

from forum.views import *


app_name = 'forum'

urlpatterns = [
    path('', SubForumListView.as_view(), name='forum'),
    path('<slug:subforum_slug>/', TopicListView.as_view(), name='subforum'),
    path('<slug:subforum_slug>/add_topic/', AddTopic.as_view(), name="add_topic"),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/', ShowTopic.as_view(), name='topic'),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/add_comment/', AddComment.as_view(), name="add_comment"),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/<int:pk>/edit_comment/', UpdateComment.as_view(), name="edit_comment"),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/<int:pk>/delete_comment/', DeleteComment.as_view(), name="delete_comment"),
]

and, finally, tests.py with further explanation:

class SubforumTestCase(TestCase):
    def setUp(self):
        self.subforum = factories.SubForumFactory()
        self.topic = factories.TopicFactory()
        self.user = factories.UserFactory()

    def test_get_topic_list(self):
        url = reverse('forum:subforum', kwargs={'subforum_slug': self.subforum.slug})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context['topics'].count(), models.Topic.objects.count())

    def test_get_topic_detail(self):
        url = reverse("forum:topic", kwargs={'subforum_slug': self.subforum.slug, 'topic_slug': self.topic.slug})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)


class CommentTestCase(TestCase):
    def setUp(self):
        self.topic = factories.TopicFactory()
        self.comment = factories.CommentFactory()
        self.user = factories.UserFactory()
        self.client.force_login(self.user)

    def test_add_comment(self):
        url = reverse("forum:add_comment", kwargs={
            'subforum_slug': self.topic.subforum.slug,
            'topic_slug': self.topic.slug
        })
        old_comments_count = Comment.objects.count()
        response = self.client.post(url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(Comment.objects.count(), 1)
        self.assertGreater(Comment.objects.count(), old_comments_count)

    def test_update_comment(self):
        url = reverse("forum:edit_comment", kwargs={
            'subforum_slug': self.topic.subforum.slug,
            'topic_slug': self.topic.slug,
            'pk': self.comment.pk
        })
        old_content = self.comment.content
        self.comment.content = 'hehehehehehehe'
        response = self.client.post(url)
        # 2 strings below - how it's supposed to work, but doesn't, so I've made a plain work-around above
        #response = self.client.post(url, {self.comment.content: 'hehehehehehehe'})
        #self.comment.refresh_from_db()

        self.assertEqual(response.status_code, 200)
        self.assertNotEqual(self.comment.content, old_content)

    def test_delete_comment(self):
        self.test_add_comment()
        url = reverse("forum:delete_comment", kwargs={
            'subforum_slug': self.topic.subforum.slug,
            'topic_slug': self.topic.slug,
            'pk': self.comment.pk
        })
        old_comments_count = Comment.objects.count()
        response = self.client.delete(url)
        self.assertEqual(response.status_code, 200)
        self.assertGreater(old_comments_count, Comment.objects.count())

-1) While SubforumTestCase tests work +/- okay, test_get_topic_list works in a strange pattern - for several failures in a row it sometimes returns an “OK” result. If it works at least once in several times, then I can pass it as a solution, but from my personal curiosity I’m interested, why does it happen this way.

Main problems are with CommentTestCase:
2a) In test_add_comment, as it sends a post-request, the response.status_code should be 302, but this way it returns an error, so I have to change the code to 200: (self.assertEqual(response.status_code, 200)). I’ve tested it with the actual server through IDE’s terminal; and, while filling the form and submitting it, the terminal shortly returns 302 code, but returns 200 code right after that. It might be connected with get_success_url’s redirect in the corresponding view, but I’m not sure whether it should work that way. Also I want to add that, before adding self.client.force_login(self.user) to setUp method, a test self.assertEqual(response.status_code, 302) returned “OK”, though some other tests didn’t.

2b) In test_add_comment self.assertGreater(Comment.objects.count(), old_comments_count) returns 1 is not greater than 1, which makes clear that no operation with the test DB has been performed (and nothing has been changed). I did it as in the tutorial and in some few examples, so I hardly understand the problem.

-3) In test_update_comment the same problem with post-request status code as in 2a) (self.assertEqual(response.status_code, 200)), but moreover, self.assertNotEqual(self.comment.content, old_content)) test returned 2 equal strings (I’ve commented 2 strings in the code that should have worked but didn’t); I’ve invented a work-around, but it’s pretty plain and stupid, as I change the value of the argument manually.
“content” - is the only changeable field of the Comment model, others are Foreign keys and some auto-generating fields like dates of creation/update.

-4) In test_delete_comment the same problem as in 2b), but vice-versa: as deletion is supposed to delete instances, so the previous amount of instances is supposed to be more numerous; but self.assertGreater(old_comments_count, Comment.objects.count()) returns 1 is not greater than 1, so, again, there is no action happened.

I’m not so well-acquainted with testing in Django (I knew only status-code tests before), so I probably do miss some important details. So I’m asking help of the community.

There is a lot of code here and it’s hard to know where to start. It’s often easier to answer questions that are specific and focused.

A couple of things that stand out to me from all this:

  • In test_add_comment() you’re making a post request, but you’re not submitting any data. In addition to the url, you need to pass a dict of form data that the view expects.
  • In the same method, you’re asserting that the number of comments is 1. But you create one comment in setUp() and then try to add another. If that works there will then be two comments.
  • In setUp() you create topic, comment, and user, but they’re not related to each other. I can’t see your models, but I’d assume a Comment has a foreign key to Topic and User? So I imagine you mean to do something more like: self.comment = factories.CommentFactory(topic=self.topic, user=self.user)? Just a guess.

I’m sure there are more things that will need to be fixed to get things working, but that’s a start.

1 Like

Mr. Philgyford,
Thank you for your answer,
Well,

  1. I thought that I have already submitted the data generated in factories, and factories’ objects are configured as attributes in setUp. If my realisation is wrong, please tell me.
  2. Actually, I didn’t know that mentioning the attributes in setUp automatically creates objects, it wasn’t obvious for me, thank you for that knowledge. So, I supposed there (self.assertGreater(Comment.objects.count(), old_comments_count)) was a comparison 1 > 0, but it actually must be 2 > 1. Still, it must work, but it doesn’t.
  3. Yep, your guess about foreign keys is right, but this connection was implemented in factories.py (the first file I provided) as SubFactory objects. So, again, if I’m wrong here, please tell me.
  1. Creating an object using a factory creates the data in the database. When you’re testing a view that requires some data from a form, it doesn’t matter whether you’ve got 0, 1 or 100 objects in the database already - your view expects some form data to be submitted.
  2. It doesn’t work because you haven’t submitted the form data to the view.
  3. Yes the factories will use the sub factories to create related objects. But those won’t be the same objects as the ones youre using in your tests. e.g. self.comment will have a topic related to it because of the sub factory in CommentFactory. You’re then creating another topic when you create self.topic. And so self.topic is unrelated to self.comment.
1 Like

Mr. Philgyford,

I got what you mean. I added data dict with the field content, and another Comment test object has been created, so test_add_object now works properly.

class SubforumTestCase(TestCase):
    def setUp(self):
        self.subforum = factories.SubForumFactory()
        self.user = factories.UserFactory()
        self.topic = factories.TopicFactory(subforum=self.subforum, creator=self.user)

    def test_get_topic_list(self):
        url = reverse('forum:subforum', kwargs={'subforum_slug': self.subforum.slug})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context['topics'].count(), models.Topic.objects.count())

    def test_get_topic_detail(self):
        url = reverse("forum:topic", kwargs={'subforum_slug': self.subforum.slug, 'topic_slug': self.topic.slug})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)


class CommentTestCase(TestCase):
    def setUp(self):
        self.subforum = factories.SubForumFactory()
        self.user = factories.UserFactory()
        self.topic = factories.TopicFactory(subforum=self.subforum, creator=self.user)
        self.comment = factories.CommentFactory(topic=self.topic, author=self.user)
        self.client.force_login(self.user)

    def test_add_comment(self):
        data = {
            'content': self.comment.content
        }
        url = reverse("forum:add_comment", kwargs={
            'subforum_slug': self.topic.subforum.slug,
            'topic_slug': self.topic.slug
        })
        old_comments_count = Comment.objects.count()
        response = self.client.post(url, data=data)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(Comment.objects.count(), 2)
        self.assertGreater(Comment.objects.count(), old_comments_count)

    def test_update_comment(self):
        url = reverse("forum:edit_comment", kwargs={
            'subforum_slug': self.topic.subforum.slug,
            'topic_slug': self.topic.slug,
            'pk': self.comment.pk
        })
        old_content = self.comment.content
        self.comment.content = 'hehehehehehehe'
        response = self.client.post(url)

        #response = self.client.post(url, {self.comment.content: 'hehehehehehehe'})
        #self.comment.refresh_from_db()

        self.assertEqual(response.status_code, 200)
        self.assertNotEqual(self.comment.content, old_content)

    def test_delete_comment(self):
        url = reverse("forum:delete_comment", kwargs={
            'subforum_slug': self.topic.subforum.slug,
            'topic_slug': self.topic.slug,
            'pk': self.comment.pk
        })
        old_comments_count = Comment.objects.count()
        response = self.client.delete(url)
        self.assertEqual(response.status_code, 302)
        self.assertGreater(old_comments_count, Comment.objects.count())

Now the last error left is that strange problem from test_update_comment.

First, in your add comment test, you have this:

You’re adding a new comment so you don’t need to post the content of the comment you’ve already created. This will work - you’ll end up with two comments in the database with the same content - but it’s a little misleading as to the meaning of the test. Just make the content “Hello” or anything else.

In the update comment test, again you’re not submitting any form data to the view.

1 Like

Finally I got what you mean.
The code below works okay:

    def test_update_comment(self):
        data = {
            'content': 'hehehehehehehe'
        }
        url = reverse("forum:edit_comment", kwargs={
            'subforum_slug': self.topic.subforum.slug,
            'topic_slug': self.topic.slug,
            'pk': self.comment.pk
        })
        old_content = self.comment.content
        response = self.client.post(url, data=data)
        self.comment.refresh_from_db()

        self.assertEqual(response.status_code, 302)
        self.assertNotEqual(self.comment.content, old_content)

I guess, that’s how it was supposed to be.
Thank you very much, mr. Philgyford!

Well done. You could assert that self.comment.content is “hehehehehehehe”, instead of the stuff with old_content.

1 Like