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.