Django测试套件运行报错:amount字段空串转数字失败
解决Django测试时的FloatField默认值转换错误
问题场景
运行如下极简Django测试用例时触发错误:
from django.test import TestCase class TestSetup(TestCase): def test_setUp(self): pdb.set_trace() # 此处代码已删除,不影响错误触发
报错信息
> 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 ''.
相关代码与迁移记录
当前Appointment模型定义
# models.py class Appointment(models.Model): amount = models.FloatField(blank=True, default=0.0)
历史迁移记录
- 曾将amount字段默认值设为空字符串:
# 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=''), ), ]
- 移除默认值:
class Migration(migrations.Migration): dependencies = [ ('giraffe', '0015_alter_appointment_amount'), ] operations = [ migrations.AlterField( model_name='appointment', name='amount', field=models.FloatField(blank=True), ), ]
- 重新设置默认值为0.0:
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), ), ]
已删除模型的迁移痕迹
还存在已删除的UnreimbursedMileage模型的迁移记录,该模型也包含amount字段,测试数据库中可能残留相关数据:
...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', ), ]
解决方案
核心原因
测试数据库执行迁移时,中间某一步将amount字段的默认值设为空字符串,后续迁移虽然修正了默认值,但PostgreSQL数据库中可能仍残留空字符串的默认值配置,导致Django尝试将空字符串转换为float时失败。
具体修复步骤
彻底清理测试数据库:
执行命令删除测试数据库(确保测试环境数据可丢弃):dropdb your_test_db_name或者通过Django命令强制重新创建:
python manage.py test --keepdb=False修正迁移文件中的错误默认值:
修改历史迁移文件中设置default=''的那一步,将默认值改为0.0,然后执行以下命令:python manage.py migrate giraffe --fake 0015 # 伪造执行到错误迁移的前一步 python manage.py migrate giraffe清理已删除模型的残留数据:
手动登录PostgreSQL测试数据库,执行SQL删除残留表:DROP TABLE IF EXISTS giraffe_unreimbursedmileage;临时绕过测试数据库迁移问题:
在settings.py中添加配置,测试时使用内存SQLite数据库:import sys if 'test' in sys.argv: DATABASES['default'] = { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:' }
内容的提问来源于stack exchange,提问作者Ron
相关产品推荐
相关产品推荐

