Search code examples
djangodjango-testingdjango-tests

Django test suite throws error on field that doesn't exist in latest migrations


The following simple test,

from django.test import TestCase
    
    class TestSetup(TestCase):   
        def test_setUp(self):
            pdb.set_trace()
            # Code here deleted, it made no difference to the error.

throws the errors:

> Destroying old test database for alias 'default'... Traceback (most
> recent call last):   File
> "/var/www/ptech/venv/lib/python3.8/site-packages/django/db/models/fields/__init__.py",
> line 1957, in get_prep_value
>     return float(value) ValueError: could not convert string to float: ''
> 
> The above exception was the direct cause of the following exception:
> 
> Traceback (most recent call last):   File "manage.py", line 22, in
> <module>
>     main()   File "manage.py", line 18, in main
>     execute_from_command_line(sys.argv)   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/core/management/__init__.py",
> line 442, in execute_from_command_line
>     utility.execute()   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/core/management/__init__.py",
> line 436, in execute
>     self.fetch_command(subcommand).run_from_argv(self.argv)   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/core/management/commands/test.py",
> line 24, in run_from_argv
>     super().run_from_argv(argv)   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/core/management/base.py",
> line 412, in run_from_argv
>     self.execute(*args, **cmd_options)   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/core/management/base.py",
> line 458, in execute
>     output = self.handle(*args, **options)   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/core/management/commands/test.py",
> line 68, in handle
>     failures = test_runner.run_tests(test_labels)   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/test/runner.py",
> line 1054, in run_tests
>     old_config = self.setup_databases(   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/test/runner.py",
> line 950, in setup_databases
>     return _setup_databases(   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/test/utils.py",
> line 221, in setup_databases
>     connection.creation.create_test_db(   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/db/backends/base/creation.py",
> line 78, in create_test_db
>     call_command(   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/core/management/__init__.py",
> line 194, in call_command
>     return command.execute(*args, **defaults)   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/core/management/base.py",
> line 458, in execute
>     output = self.handle(*args, **options)   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/core/management/base.py",
> line 106, in wrapper
>     res = handle_func(*args, **kwargs)   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/core/management/commands/migrate.py",
> line 356, in handle
>     post_migrate_state = executor.migrate(   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/db/migrations/executor.py",
> line 135, in migrate
>     state = self._migrate_all_forwards(   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/db/migrations/executor.py",
> line 167, in _migrate_all_forwards
>     state = self.apply_migration(   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/db/migrations/executor.py",
> line 252, in apply_migration
>     state = migration.apply(state, schema_editor)   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/db/migrations/migration.py",
> line 132, in apply
>     operation.database_forwards(   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/db/migrations/operations/fields.py",
> line 235, in database_forwards
>     schema_editor.alter_field(from_model, from_field, to_field)   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/db/backends/base/schema.py",
> line 830, in alter_field
>     self._alter_field(   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/db/backends/postgresql/schema.py",
> line 287, in _alter_field
>     super()._alter_field(   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/db/backends/base/schema.py",
> line 1025, in _alter_field
>     new_default = self.effective_default(new_field)   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/db/backends/base/schema.py",
> line 429, in effective_default
>     return field.get_db_prep_save(self._effective_default(field), self.connection)   File
> "/var/www/ptech/venv/lib/python3.8/site-packages/django/db/models/fields/__init__.py",
> line 954, in get_db_prep_save
>     return self.get_db_prep_value(value, connection=connection, prepared=False)   File
> "/var/www/ptech/venv/lib/python3.8/site-packages/django/db/models/fields/__init__.py",
> line 947, in get_db_prep_value
>     value = self.get_prep_value(value)   File "/var/www/ptech/venv/lib/python3.8/site-packages/django/db/models/fields/__init__.py",
> line 1959, in get_prep_value
>     raise e.__class__( ValueError: Field 'amount' expected a number but got ''.

The field 'amount' does exist, but in a model that isn't loaded for any tests:

models.py
    
class Appointment(models.Model):
    amount = models.FloatField(blank=True, default=0.0)

But just in case it's instantiating that model in the process of creating the test database, I migrated "default=0.0" to "default='0.0' " (Single quotes around the value). Same error.

I also tried deleting the postgresql test database manually with psql.

I'm initiating the test with

python manage.py test

and also by specifying the app_name:

python manage.py test giraffe

from the project directory.

Relevant migrations, in sequential order:

# Generated by Django 4.1.3 on 2023-01-10 22:58
from django.db import migrations, models   

class Migration(migrations.Migration):
    dependencies = [
        ('giraffe', '0014_appointment_notes'),
    ]
    operations = [
        migrations.AlterField(
            model_name='appointment',
            name='amount',
            field=models.FloatField(blank=True, default=''),
        ),
    ]

and

class Migration(migrations.Migration):
    dependencies = [
        ('giraffe', '0015_alter_appointment_amount'),
    ]
    operations = [
        migrations.AlterField(
            model_name='appointment',
            name='amount',
            field=models.FloatField(blank=True),
        ),
    ]

and

class Migration(migrations.Migration):
    dependencies = [
        ('giraffe', '0016_alter_appointment_amount'),
    ]
    operations = [
        migrations.AlterField(
            model_name='appointment',
            name='amount',
            field=models.FloatField(blank=True, default=0.0),
        ),
    ]

And some earlier migrations pertaining to a model with an 'amount' field, but a model which was deleted, but still shows up in the test database.

...migrations.CreateModel(
            name='UnreimbursedMileage',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('mileage_rate_category', models.CharField(choices=[('FB22a', 'Federal Business Rate, Qtr. 1&2 2022'), ('FB22b', 'Federal Business Rate, Qtr. 3&4, 2022'), ('FB23', 'Federal Business Rate 2023')], default='FB22a', max_length=5)),
                ('num_miles', models.FloatField(blank=True, default=0)),
                ('amount', models.FloatField(blank=True, null=True)),
                ('for_appointment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='giraffe.appointment')),
            ],
            options={
                'verbose_name_plural': 'Unreimbursed Mileage',
            },
        ),

class Migration(migrations.Migration):
    dependencies = [
        ('giraffe', '0054_rename_to_do_next_time_todonexttime_and_more'),
    ]
    operations = [
        migrations.RemoveField(
            model_name='unreimbursedmileage',
            name='for_appointment',
        ),
        migrations.DeleteModel(
            name='Dummy',
        ),
        migrations.DeleteModel(
            name='UnreimbursedMileage',
        ),
    ]

Solution

  • The original question as posted is not about how to reset migrations, and NOT a duplicate of other SO questions on how to reset migrations. It's about gaining deeper understanding of any underlying mechanisms which might allow the original migrations to execute flawlessly despite the errors in the code illustrated in the question, then fail in unit test. All 76+ migrations in the history show up as successfully executed when listed by 'showmigrations', but now (properly) fail when a unit test database is generated.

    The hope of the question is that a fix can be found without doing a migration reset.

    It's as if data isn't applied to models defined in original migrations - as if model field definitions' word is taken for it - but then problems can still arise when actually instantiated in test. However, I can't imagine that such a hole would exist in Django. In any case, the scenario described in the question is a good argument for TDD.

    Along the way, it was hoped that the nuances that can arise given the sequential, inter-dependent nature of migrations might be made clearer for any new Django users who might read this in the future, i.e. it's a little more complicated than 'just fix that one migration'.

    But the bottom line is, whatever the underlying mechanisms or actions that led to the problem, the present action to be done is to execute a reset of migrations and generate a new initial migration. The recommended method for doing that is at How to Reset Migrations .