Home > Back-end >  How to test Django model fields
How to test Django model fields

Time:11-04

I want to do unit tests for Django model fields:

  class Animal(models.Model):
    name = models.CharField(unique=True, max_length=30, blank=False, null=False)
    class Meta:
        managed = True
        db_table = 'animal'
        ordering = ['name']

My Test setUp looks like this:

class TestAnimalModel(TestCase):
    def setUp(self):
        self.animal = Animal.objects.create(name = 'TestAnimal')
        self.animal2 = Animal.objects.create(name = 123)
        self.animal3 = Animal.objects.create(name = 'TestAnimalWithTooManyCharacters')

animal2 and animal3 are created even though they shouldn't because animal2.name is int and animal3.name has more than 30 characters.
How then do I test if name field has proper values?
Is it normal that the object is created even though it has values that doesn't match Model?

CodePudding user response:

they shouldn't because animal2.name is int

A CharField will call str(…) on what is passed as value. This can sometimes have unintended side-effects. But Django fields are designed to accept a variety of values. For example for a DateField, it also accepts a date formatted as YYYY-MM-DD. We can see this in the implementation of a CharField [GitHub]:

class CharField(Field):
    # …

    def to_python(self, value):
        """Return a string."""
        if value not in self.empty_values:
            value = str(value)
            if self.strip:
                value = value.strip()
        if value in self.empty_values:
            return self.empty_value
        return value

animal3.name has more than 30 characters.

If I run this with MySQL I get a:

>>> Animal.objects.create(name='TestAnimalWithTooManyCharacters')
Traceback (most recent call last):
  File "/django/env/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/django/env/lib/python3.8/site-packages/django/db/backends/mysql/base.py", line 73, in execute
    return self.cursor.execute(query, args)
  File "/django/env/lib/python3.8/site-packages/MySQLdb/cursors.py", line 206, in execute
    res = self._query(query)
  File "/django/env/lib/python3.8/site-packages/MySQLdb/cursors.py", line 319, in _query
    db.query(q)
  File "/django/env/lib/python3.8/site-packages/MySQLdb/connections.py", line 259, in query
    _mysql.connection.query(self, query)
MySQLdb._exceptions.DataError: (1406, "Data too long for column 'name' at row 1")

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/django/env/lib/python3.8/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/django/env/lib/python3.8/site-packages/django/db/models/query.py", line 453, in create
    obj.save(force_insert=True, using=self.db)
  File "/django/env/lib/python3.8/site-packages/django/db/models/base.py", line 726, in save
    self.save_base(using=using, force_insert=force_insert,
  File "/django/env/lib/python3.8/site-packages/django/db/models/base.py", line 763, in save_base
    updated = self._save_table(
  File "/django/env/lib/python3.8/site-packages/django/db/models/base.py", line 868, in _save_table
    results = self._do_insert(cls._base_manager, using, fields, returning_fields, raw)
  File "/django/env/lib/python3.8/site-packages/django/db/models/base.py", line 906, in _do_insert
    return manager._insert(
  File "/django/env/lib/python3.8/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/django/env/lib/python3.8/site-packages/django/db/models/query.py", line 1270, in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
  File "/django/env/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 1410, in execute_sql
    cursor.execute(sql, params)
  File "/django/env/lib/python3.8/site-packages/django/db/backends/utils.py", line 98, in execute
    return super().execute(sql, params)
  File "/django/env/lib/python3.8/site-packages/django/db/backends/utils.py", line 66, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/django/env/lib/python3.8/site-packages/django/db/backends/utils.py", line 75, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/django/env/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/django/env/lib/python3.8/site-packages/django/db/utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/django/env/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/django/env/lib/python3.8/site-packages/django/db/backends/mysql/base.py", line 73, in execute
    return self.cursor.execute(query, args)
  File "/django/env/lib/python3.8/site-packages/MySQLdb/cursors.py", line 206, in execute
    res = self._query(query)
  File "/django/env/lib/python3.8/site-packages/MySQLdb/cursors.py", line 319, in _query
    db.query(q)
  File "/django/env/lib/python3.8/site-packages/MySQLdb/connections.py", line 259, in query
    _mysql.connection.query(self, query)
django.db.utils.DataError: (1406, "Data too long for column 'name' at row 1")

Depending on the database you use, it might (not) be accepted. SQLite for example does not enforce length constraints. Indeed, as the FAQ of SQLite says:

(9) What is the maximum size of a VARCHAR in SQLite?

SQLite does not enforce the length of a VARCHAR. You can declare a VARCHAR(10) and SQLite will be happy to store a 500-million character string there. And it will keep all 500-million characters intact. Your content is never truncated. SQLite understands the column type of "VARCHAR(N)" to be the same as "TEXT", regardless of the value of N.

So SQLite will not validate it.

You can let the Django validators run however before saving it to the database, for example with:

animal = Animal(name='TestAnimalWithTooManyCharacters')
animal.full_clean()
animal.save()

This will let the Django validators run and raise exceptions if the data is not valid:

>>> animal = Animal(name='TestAnimalWithTooManyCharacters')
>>> animal.full_clean()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/django/env/lib/python3.8/site-packages/django/db/models/base.py", line 1238, in full_clean
    raise ValidationError(errors)
django.core.exceptions.ValidationError: {'name': ['Ensure this value has at most 30 characters (it has 31).']}

A Django ModelForm will also check this and reject the data passed to the form in case the string is too long.

  • Related