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
isint
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 aVARCHAR(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 ofN
.
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.