Search code examples
djangodjango-formspytestdjango-testing

Django form test fails


I'm trying to perform a simple test on my form to confirm that it's not valid when there is no data given and is valid when data is given.

When running tests with pytest (py.test) the test with no data works fine but I'm getting this error for the test with data present:

AssertionError: Should be valid if data is given
E       assert False is True
E        +  where False = <bound method BaseForm.is_valid of <PostForm bound=True, valid=False, fields=(title;content;author;image;published;draft;category;read_time)>>()
E        +    where <bound method BaseForm.is_valid of <PostForm bound=True, valid=False, fields=(title;content;author;image;published;draft;category;read_time)>> = <PostForm bound=True, valid=False, fields=(title;content;author;image;published;draft;category;read_time)>.is_valid

posts/tests/test_forms.py:21: AssertionError

my models.py:

from django.db import models
from django.core.urlresolvers import reverse
from django.conf import settings
from django.db.models.signals import pre_save
from django.utils import timezone

from django.utils.text import slugify
from .utils import read_time

class Category(models.Model):
    name = models.CharField(max_length=120, unique=True)
    timestamp = models.DateTimeField(auto_now_add=True, auto_now=False)
    updated = models.DateTimeField(auto_now_add=False, auto_now=True)
    slug = models.SlugField(unique=True)

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if not self.id: # to prevent changing slug on updates
            self.slug = slugify(self.name)
        return super(Category, self).save(*args, **kwargs)

def upload_location(instance, filename):
    return '%s/%s'%(instance.id, filename)

class PostManager(models.Manager):
    def active(self):
        return super(PostManager, self).filter(draft=False, published__lte=timezone.now())

class Post(models.Model):
    title = models.CharField(max_length=120)
    slug = models.SlugField(unique=True)
    content = models.TextField()
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    image = models.ImageField(
        upload_to=upload_location,
        null=True,
        blank=True)
    timestamp = models.DateTimeField(auto_now_add=True, auto_now=False)
    updated = models.DateTimeField(auto_now_add=False, auto_now=True)
    published = models.DateField(auto_now=False, auto_now_add=False)
    draft = models.BooleanField(default=False)
    category = models.ManyToManyField(Category)
    read_time = models.IntegerField(default=0)
    objects = PostManager()

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('posts:detail', kwargs={'pk': self.pk})

    def save_no_img(self):
        self.image = None
        return super(Post, self).save()


def create_slug(instance, new_slug=None):
    slug = slugify(instance.title)
    if new_slug is not None:
        slug = new_slug
    qs = Post.objects.filter(slug=slug).order_by("-id")
    exists = qs.exists()
    if exists:
        new_slug = "%s-%s" %(slug, qs.first().id)
        return create_slug(instance, new_slug=new_slug)
    return slug


def pre_save_post_receiver(sender, instance, *args, **kwargs):
    if not instance.slug:
        instance.slug = create_slug(instance)
    html_content = instance.content
    instance.read_time = read_time(html_content)

pre_save.connect(pre_save_post_receiver, sender=Post)

my forms.py:

from django import forms
from .models import Post
from pagedown.widgets import PagedownWidget

class PostForm(forms.ModelForm):
    published = forms.DateField(widget=forms.SelectDateWidget)
    content = forms.CharField(widget=PagedownWidget())
    class Meta:
        model = Post
        # fields = ['author', 'title', 'content', 'image', 'draft', 'published', 'category']
        exclude = ['objects', 'updated', 'timestamp', 'slug']

test_forms.py:

import pytest
from .. import forms
from posts.models import Category
from mixer.backend.django import mixer
pytestmark = pytest.mark.django_db


class TestPostForm():
    def test_empty_form(self):
        form = forms.PostForm(data={})
        assert form.is_valid() is False, 'Should be invalid if no data is given'

    def test_not_empty_form(self):
        staff_user = mixer.blend('auth.User', is_staff=True)
        category = mixer.blend('posts.Category')
        data={'content': 'some content',
            'author': staff_user,
            'title': 'some title',
            'category': category,}
        form = forms.PostForm(data=data)
        assert form.is_valid() is True, 'Should be valid if data is given'

update: collected more specific errors using:

assert form.errors == {}, 'should be empty'

errors:

{'author': ['Select a valid choice. That choice is not one of the 
available choices.'],
'category': ['Enter a list of values.'],
'published': ['This field is required.'],
'read_time': ['This field is required.']}

how to address them?

update 2: as Nadège suggested I modified data to include published and read_time, changed category into a list and created a user without mixer.

staff_user = User.objects.create_superuser(is_staff=True,
                                        email='[email protected]',
                                        username='staffuser',
                                        password='somepass')
category = mixer.blend('posts.Category')
today = date.today()
data={'content': 'some content',
    'author': staff_user,
    'title': 'some title',
    'published': today,
    'read_time': 1,
    'category': [category],}

There is still error regarding the 'author':

{'author': ['Select a valid choice. That choice is not one of the available choices.']}

update 3: for some reason 'author' had to be provided as an id, the working code for this test looks like this:

class TestPostForm():
    def test_empty_form(self):
        form = forms.PostForm(data={})
        assert form.is_valid() is False, 'Should be invalid if no data is given'

    def test_not_empty_form(self):
        staff_user = mixer.blend('auth.User')
        category = mixer.blend('posts.Category')
        today = date.today()
        data={'content': 'some content',
            'author': staff_user.id,
            'title': 'some title',
            'published': today,
            'read_time': 1,
            'category': [category],}
        form = forms.PostForm(data=data)
        assert form.errors == {}, 'shoud be empty'
        assert form.is_valid() is True, 'Should be valid if data is given'

Solution

  • Ok so when you have an invalid form, first thing is to check why, so the errors of the form. With this new information we can fix each problem. Your form has 4 validations errors. The last two are pretty straightforward.

    'published': ['This field is required.'],
    'read_time': ['This field is required.']
    

    Those two fields in your form are required but you didn't filled them. So you have two options,

    • Add a value for those fields in the data you give to the form
    • Remove the fields from the form: add them to exclude

    You can also set the published field a not required like this:

    published = forms.DateField(widget=forms.SelectDateWidget, required=False)
    

    for read_time, the field is required or not, depending on the corresponding field in the model. If the model field is not nullable, the field in the form is set as required.

    Next there is

    'category': ['Enter a list of values.']
    

    You provided a value but the type is not what was expected. category in your model is ManyToMany so you can't give just one category, it must be a list (even if it has only one element!)

    'category': [category],
    

    Finally the author,

    'author': ['Select a valid choice. That choice is not one of the available choices.']
    

    There too you provided a value that is not valid. The validation doesn't recognize the value as a proper auth.User. I'm not familiar with Mixer, so maybe ask a new question specifically about Mixer and Django Forms with a ForeignKey.