Gitlab CI/CD 管道在迁移之前运行 Django 单元测试

Gitlab CI/CD Pipeline Runs Django Unit Tests Before Migrations

问题

我正在尝试在 Gitlab 的 CI/CD 中设置一个测试阶段。在本地,运行 单元测试正常进行并符合预期。但是,在 Gitlab 的 CI/CD 中,当 运行ning 脚本 coverage run manage.py test -v 2 && coverage report 时,单元测试正在执行 before迁移在测试数据库中完成,这是意想不到的,并且总是会失败。测试数据库上的迁移需要 运行 在单元测试执行之前。

知道为什么会发生这种行为吗?

当 运行 宁 python manage.py test 这些是默认发生的步骤:

  1. 创建测试数据库。

  2. 数据库迁移。

  3. 运行 系统检查。

  4. 运行正在测试。

  5. 报告测试次数和成功/失败。

  6. 正在删除测试数据库。

在下面的 本地测试 运行 输出 中,您可以看到这些确切的步骤,按顺序发生。问题是,在 Gitlab 的 CI/CD 管道中,第 2 步和第 4 步由于某种神秘原因被切换了。

我已经尝试过的东西

Gitlab CI/CD 输出

我的实际产量要大得多。但这里是相关部分:

$ echo $TEST_SECRETS | base64 -d > config/settings/local.py
$ echo RUNNING UNIT TESTS
RUNNING UNIT TESTS
$ coverage run manage.py test -v 2 && coverage report
Creating test database for alias 'default' ('test_mia')...
test_dataconnector_category_creation_delete (apps.core.tests.test_models.DataConnectorCategoryTestCase)
Verify data connector category creation ... ok
test_compare_airbytesetting (apps.services.tests.test_model_airbytesetting.AirbyteSettingModel_TestClass)
Compare object from self.airbytesetting to ensure data is in proper format. ... ok
test_fieldconstraints_airbytesetting (apps.services.tests.test_model_airbytesetting.AirbyteSettingModel_TestClass)
Compares Field Constraint Values for Accuracy ... ok
test_update_airbytesetting (apps.services.tests.test_model_airbytesetting.AirbyteSettingModel_TestClass)
Test updating values in self.airbytesetting and saving that to the test database. ... ok
test_compare_airbytesource (apps.services.tests.test_model_airbytesource.AirbyteSourceModel_TestClass)
Compare object from self.airbytesource to ensure data is in proper format. ... ok
test_delete_airbytesource (apps.services.tests.test_model_airbytesource.AirbyteSourceModel_TestClass)
Test Deleting self.airbytesource object from database ... ok
test_fieldconstraints_airbytesource (apps.services.tests.test_model_airbytesource.AirbyteSourceModel_TestClass)
Compares Field Constraint Values for Accuracy ... ok
test_update_airbytesource (apps.services.tests.test_model_airbytesource.AirbyteSourceModel_TestClass)
Test updating values in self.airbytesource and saving that to the test database. ... ok
test_compare_airbytestream (apps.services.tests.test_model_airbytestream.AirbyteStreamModel_TestClass)
Compare object from self.airbytestream to ensure data is in proper format. ... ok
test_delete_airbytestream (apps.services.tests.test_model_airbytestream.AirbyteStreamModel_TestClass)
Test Deleting self.airbytestream object from database ... ok
test_fieldconstraints_airbytestream (apps.services.tests.test_model_airbytestream.AirbyteStreamModel_TestClass)
Compares Field Constraint Values for Accuracy ... ok
test_update_airbytestream (apps.services.tests.test_model_airbytestream.AirbyteStreamModel_TestClass)
Test updating values in self.airbytestream and saving that to the test database. ... ok
test_compare_airbytesynccatalog (apps.services.tests.test_model_airbytesynccatalog.AirbyteSyncCatalogModel_TestClass)
Compare object from self.airbytesynccatalog to ensure data is in proper format. ... ok
test_delete_airbytesynccatalog (apps.services.tests.test_model_airbytesynccatalog.AirbyteSyncCatalogModel_TestClass)
Test Deleting self.airbytesynccatalog object from database ... ok
test_fieldconstraints_airbytesynccatalog (apps.services.tests.test_model_airbytesynccatalog.AirbyteSyncCatalogModel_TestClass)
Compares Field Constraint Values for Accuracy ... ok
test_update_airbytesynccatalog (apps.services.tests.test_model_airbytesynccatalog.AirbyteSyncCatalogModel_TestClass)
Test updating values in self.airbytesynccatalog and saving that to the test database. ... ok
test_compare_tableausetting (apps.services.tests.test_model_tableausetting.TableauSettingModel_TestClass)
Compare object from self.tableausetting to ensure data is in proper format. ... ok
test_createrandom_tableausetting (apps.services.tests.test_model_tableausetting.TableauSettingModel_TestClass)
Test Creating a New (Second) TableauSetting Model Object with Random Values. ... ok
test_delete_tableausetting (apps.services.tests.test_model_tableausetting.TableauSettingModel_TestClass)
Test Deleting self.tableausetting object from database ... ok
test_fieldconstraints_tableausetting (apps.services.tests.test_model_tableausetting.TableauSettingModel_TestClass)
Compares Field Constraint Values for Accuracy ... ok
test_update_tableausetting (apps.services.tests.test_model_tableausetting.TableauSettingModel_TestClass)
Test updating values in self.tableausetting and saving that to the test database. ... ok
test_get_namespace_custom_format (apps.services.tests.test_scripts_airbyte.ServicesTestCase)
TO-DO: this method should live in AirByte class ... ok
test_retrieve_me_user (apps.user.tests.test_views.UserViewTest) ... ok
test_retrieve_user (apps.user.tests.test_views.UserViewTest) ... ok
apps.user.tests.test_models (unittest.loader._FailedTest) ... ERROR
======================================================================
ERROR: apps.user.tests.test_models (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: apps.user.tests.test_models
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
psycopg2.errors.UndefinedTable: relation "user_user" does not exist
LINE 1: INSERT INTO "user_user" ("password", "last_login", "is_super...
                    ^
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/unittest/loader.py", line 436, in _find_test_path
    module = self._get_module_from_name(name)
  File "/usr/local/lib/python3.8/unittest/loader.py", line 377, in _get_module_from_name
    __import__(name)
  File "/builds/americommerce/mia/mia-ms-main/apps/user/tests/test_models.py", line 3, in <module>
    from apps.user.tests.factories import UserFactory, CompanyFactory, SignUpTokenFactory
  File "/builds/americommerce/mia/mia-ms-main/apps/user/tests/factories.py", line 23, in <module>
    class SignUpTokenFactory(factory.django.DjangoModelFactory):
  File "/builds/americommerce/mia/mia-ms-main/apps/user/tests/factories.py", line 28, in SignUpTokenFactory
    user = UserFactory()
  File "/usr/local/lib/python3.8/site-packages/factory/base.py", line 40, in __call__
    return cls.create(**kwargs)
  File "/usr/local/lib/python3.8/site-packages/factory/base.py", line 528, in create
    return cls._generate(enums.CREATE_STRATEGY, kwargs)
  File "/usr/local/lib/python3.8/site-packages/factory/django.py", line 117, in _generate
    return super()._generate(strategy, params)
  File "/usr/local/lib/python3.8/site-packages/factory/base.py", line 465, in _generate
    return step.build()
  File "/usr/local/lib/python3.8/site-packages/factory/builder.py", line 262, in build
    instance = self.factory_meta.instantiate(
  File "/usr/local/lib/python3.8/site-packages/factory/base.py", line 317, in instantiate
    return self.factory._create(model, *args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/factory/django.py", line 166, in _create
    return manager.create(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/django/db/models/query.py", line 447, in create
    obj.save(force_insert=True, using=self.db)
  File "/usr/local/lib/python3.8/site-packages/django/contrib/auth/base_user.py", line 67, in save
    super().save(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/django/db/models/base.py", line 753, in save
    self.save_base(using=using, force_insert=force_insert,
  File "/usr/local/lib/python3.8/site-packages/django/db/models/base.py", line 790, in save_base
    updated = self._save_table(
  File "/usr/local/lib/python3.8/site-packages/django/db/models/base.py", line 895, in _save_table
    results = self._do_insert(cls._base_manager, using, fields, returning_fields, raw)
  File "/usr/local/lib/python3.8/site-packages/django/db/models/base.py", line 933, in _do_insert
    return manager._insert(
  File "/usr/local/lib/python3.8/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/django/db/models/query.py", line 1254, in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
  File "/usr/local/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 1397, in execute_sql
    cursor.execute(sql, params)
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/integrations/django/__init__.py", line 508, in execute
    return real_execute(self, sql, params)
  File "/usr/local/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 "/usr/local/lib/python3.8/site-packages/django/db/backends/utils.py", line 75, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/usr/local/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/usr/local/lib/python3.8/site-packages/django/db/utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/usr/local/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
django.db.utils.ProgrammingError: relation "user_user" does not exist
LINE 1: INSERT INTO "user_user" ("password", "last_login", "is_super...
                    ^
----------------------------------------------------------------------
Ran 25 tests in 0.648s
FAILED (errors=1)
Destroying test database for alias 'default' ('test_mia')...
Operations to perform:
  Synchronize unmigrated apps: corsheaders, debug_toolbar, delete_migrations, django_filters, generic_relations, messages, mptt, rest_framework, rest_framework_jwt, rest_framework_swagger, staticfiles
  Apply all migrations: admin, auth, contenttypes, core, dashboard, reports, services, sessions, store, user
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0001_initial... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying user.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying store.0001_initial... OK
  Applying dashboard.0001_initial... OK
  Applying core.0001_initial... OK
  Applying reports.0001_initial... OK
  Applying reports.0002_emailreport_store... OK
  Applying reports.0003_initial... OK
  Applying reports.0004_remove_emailreport_segments... OK
  Applying core.0002_dataconnectorcategory... OK
  Applying core.0003_auto_20220331_1610... OK
  Applying core.0004_delete_segment... OK
  Applying dashboard.0002_auto_20220331_1804... OK
  Applying dashboard.0003_flag_tableauview_widget_widgetsection... OK
  Applying reports.0005_emailreport_segments... OK
  Applying services.0001_initial... OK
  Applying services.0002_auto_20220325_0426... OK
  Applying services.0003_auto_20220331_1610... OK
  Applying sessions.0001_initial... OK
  Applying store.0002_initial... OK
  Applying store.0003_storefrontdataconnector... OK
  Applying user.0002_auto_20220325_0426... OK
  Applying user.0003_auto_20220331_1610... OK
  Applying user.0004_auto_20220331_1804... OK
  Applying user.0005_user_flags... OK
System check identified no issues (0 silenced).
Cleaning up project directory and file based variables
00:01
ERROR: Job failed: exit code 1

本地测试运行输出

Creating test database for alias 'default' ('test_mia')...
Operations to perform:
  Synchronize unmigrated apps: corsheaders, debug_toolbar, delete_migrations, django_filters, generic_relations, messages, mptt, rest_framework, rest_framework_jwt, rest_framework_swagger, staticfiles
  Apply all migrations: admin, auth, contenttypes, core, dashboard, reports, services, sessions, store, user
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0001_initial... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying user.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying store.0001_initial... OK
  Applying dashboard.0001_initial... OK
  Applying core.0001_initial... OK
  Applying reports.0001_initial... OK
  Applying reports.0002_emailreport_store... OK
  Applying reports.0003_initial... OK
  Applying reports.0004_remove_emailreport_segments... OK
  Applying core.0002_dataconnectorcategory... OK
  Applying core.0003_auto_20220331_1610... OK
  Applying core.0004_delete_segment... OK
  Applying dashboard.0002_auto_20220331_1804... OK
  Applying dashboard.0003_flag_tableauview_widget_widgetsection... OK
  Applying reports.0005_emailreport_segments... OK
  Applying services.0001_initial... OK
  Applying services.0002_auto_20220325_0426... OK
  Applying services.0003_auto_20220331_1610... OK
  Applying sessions.0001_initial... OK
  Applying store.0002_initial... OK
  Applying store.0003_storefrontdataconnector... OK
  Applying user.0002_auto_20220325_0426... OK
  Applying user.0003_auto_20220331_1610... OK
  Applying user.0004_auto_20220331_1804... OK
  Applying user.0005_user_flags... OK
System check identified no issues (0 silenced).
test_dataconnector_category_creation_delete (apps.core.tests.test_models.DataConnectorCategoryTestCase)
Verify data connector category creation ... ok
test_compare_airbytesetting (apps.services.tests.test_model_airbytesetting.AirbyteSettingModel_TestClass)
Compare object from self.airbytesetting to ensure data is in proper format. ... ok
test_fieldconstraints_airbytesetting (apps.services.tests.test_model_airbytesetting.AirbyteSettingModel_TestClass)
Compares Field Constraint Values for Accuracy ... ok
test_update_airbytesetting (apps.services.tests.test_model_airbytesetting.AirbyteSettingModel_TestClass)
Test updating values in self.airbytesetting and saving that to the test database. ... ok
test_compare_airbytesource (apps.services.tests.test_model_airbytesource.AirbyteSourceModel_TestClass)
Compare object from self.airbytesource to ensure data is in proper format. ... ok
test_delete_airbytesource (apps.services.tests.test_model_airbytesource.AirbyteSourceModel_TestClass)
Test Deleting self.airbytesource object from database ... ok
test_fieldconstraints_airbytesource (apps.services.tests.test_model_airbytesource.AirbyteSourceModel_TestClass)
Compares Field Constraint Values for Accuracy ... ok
test_update_airbytesource (apps.services.tests.test_model_airbytesource.AirbyteSourceModel_TestClass)
Test updating values in self.airbytesource and saving that to the test database. ... ok
test_compare_airbytestream (apps.services.tests.test_model_airbytestream.AirbyteStreamModel_TestClass)
Compare object from self.airbytestream to ensure data is in proper format. ... ok
test_delete_airbytestream (apps.services.tests.test_model_airbytestream.AirbyteStreamModel_TestClass)
Test Deleting self.airbytestream object from database ... ok
test_fieldconstraints_airbytestream (apps.services.tests.test_model_airbytestream.AirbyteStreamModel_TestClass)
Compares Field Constraint Values for Accuracy ... ok
test_update_airbytestream (apps.services.tests.test_model_airbytestream.AirbyteStreamModel_TestClass)
Test updating values in self.airbytestream and saving that to the test database. ... ok
test_compare_airbytesynccatalog (apps.services.tests.test_model_airbytesynccatalog.AirbyteSyncCatalogModel_TestClass)
Compare object from self.airbytesynccatalog to ensure data is in proper format. ... ok
test_delete_airbytesynccatalog (apps.services.tests.test_model_airbytesynccatalog.AirbyteSyncCatalogModel_TestClass)
Test Deleting self.airbytesynccatalog object from database ... ok
test_fieldconstraints_airbytesynccatalog (apps.services.tests.test_model_airbytesynccatalog.AirbyteSyncCatalogModel_TestClass)
Compares Field Constraint Values for Accuracy ... ok
test_update_airbytesynccatalog (apps.services.tests.test_model_airbytesynccatalog.AirbyteSyncCatalogModel_TestClass)
Test updating values in self.airbytesynccatalog and saving that to the test database. ... ok
test_compare_tableausetting (apps.services.tests.test_model_tableausetting.TableauSettingModel_TestClass)
Compare object from self.tableausetting to ensure data is in proper format. ... ok
test_createrandom_tableausetting (apps.services.tests.test_model_tableausetting.TableauSettingModel_TestClass)
Test Creating a New (Second) TableauSetting Model Object with Random Values. ... ok
test_delete_tableausetting (apps.services.tests.test_model_tableausetting.TableauSettingModel_TestClass)
Test Deleting self.tableausetting object from database ... ok
test_fieldconstraints_tableausetting (apps.services.tests.test_model_tableausetting.TableauSettingModel_TestClass)
Compares Field Constraint Values for Accuracy ... ok
test_update_tableausetting (apps.services.tests.test_model_tableausetting.TableauSettingModel_TestClass)
Test updating values in self.tableausetting and saving that to the test database. ... ok
test_get_namespace_custom_format (apps.services.tests.test_scripts_airbyte.ServicesTestCase)
TO-DO: this method should live in AirByte class ... ok
test_company_creation (apps.user.tests.test_models.UserTestCase)
Verify Company creation ... ok
test_signup_token_creation (apps.user.tests.test_models.UserTestCase)
Verify token creation ... ok
test_user_creation (apps.user.tests.test_models.UserTestCase)
Verify User creation ... ok
test_user_creation_with_company (apps.user.tests.test_models.UserTestCase)
Verify User can be assigned company ... ok
test_user_creation_with_signup_token (apps.user.tests.test_models.UserTestCase)
Verify User can be assigned token ... ok
test_retrieve_me_user (apps.user.tests.test_views.UserViewTest) ... ok
test_retrieve_user (apps.user.tests.test_views.UserViewTest) ... ok

----------------------------------------------------------------------
Ran 29 tests in 0.521s

OK
Destroying test database for alias 'default' ('test_mia')...
Name                                              Stmts   Miss  Cover
---------------------------------------------------------------------
apps/core/admin.py                                    5      0   100%
apps/core/exceptions.py                              22      3    86%
apps/core/mixins.py                                  10      3    70%
apps/core/models.py                                   8      1    88%
apps/core/serializers.py                             18      0   100%
apps/core/urls.py                                     5      0   100%
apps/core/utils.py                                   69     33    52%
apps/core/views.py                                   42     26    38%
apps/dashboard/admin.py                              15      0   100%
apps/dashboard/models.py                             34      7    79%
apps/dashboard/urls.py                                6      0   100%
apps/dashboard/views.py                              27     16    41%
apps/delete_migrations/admin.py                       1      0   100%
apps/delete_migrations/models.py                      1      0   100%
apps/reports/admin.py                                14      0   100%
apps/reports/mixins.py                               16      9    44%
apps/reports/models/alert.py                         47      3    94%
apps/reports/models/email_report.py                  31      1    97%
apps/reports/serializers/alert.py                    49     14    71%
apps/reports/serializers/email_report.py             49     22    55%
apps/reports/tasks.py                                71     43    39%
apps/reports/urls.py                                  9      0   100%
apps/reports/utils.py                                12      9    25%
apps/reports/views/alerts.py                         40     23    42%
apps/reports/views/reports.py                        55     26    53%
apps/services/admin.py                               23      6    74%
apps/services/models/airbyte.py                      37      0   100%
apps/services/models/tableau.py                       9      0   100%
apps/services/scripts/airbyte.py                    157    126    20%
apps/services/scripts/tableau.py                     69     50    28%
apps/services/urls.py                                 3      0   100%
apps/services/views.py                               15      8    47%
apps/store/admin.py                                  41      0   100%
apps/store/mixins.py                                 12      7    42%
apps/store/models/amazon_ads.py                      18      2    89%
apps/store/models/amazon_seller_partner.py           21      2    90%
apps/store/models/data_connector_base.py             28      5    82%
apps/store/models/data_connector_settings.py         18      3    83%
apps/store/models/facebook.py                        17      2    88%
apps/store/models/google_ads.py                      20      2    90%
apps/store/models/google_analytics.py                18      2    89%
apps/store/models/google_search_console.py           18      2    89%
apps/store/models/hubspot.py                         14      2    86%
apps/store/models/klaviyo.py                         12      1    92%
apps/store/models/mailchimp.py                       14      2    86%
apps/store/models/shopify.py                         17      2    88%
apps/store/models/store.py                           13      3    77%
apps/store/models/storefront.py                      19      8    58%
apps/store/serializers/amazon_ads.py                 22     11    50%
apps/store/serializers/amazon_seller_partner.py      26     14    46%
apps/store/serializers/base.py                        8      0   100%
apps/store/serializers/credentials.py                 3      0   100%
apps/store/serializers/facebook.py                   15      7    53%
apps/store/serializers/google_ads.py                 23     12    48%
apps/store/serializers/google_analytics.py           22     11    50%
apps/store/serializers/google_search_console.py      22     11    50%
apps/store/serializers/hubspot.py                     6      0   100%
apps/store/serializers/klaviyo.py                     6      0   100%
apps/store/serializers/list_data_connectors.py       24      0   100%
apps/store/serializers/mailchimp.py                   6      0   100%
apps/store/serializers/shopify.py                    21     10    52%
apps/store/serializers/store.py                      30      3    90%
apps/store/serializers/storefront.py                  6      0   100%
apps/store/tasks.py                                 104     80    23%
apps/store/urls.py                                   26      0   100%
apps/store/utils.py                                  89     66    26%
apps/store/views/check_data_connector_store.py       21     13    38%
apps/store/views/credentials.py                      13      3    77%
apps/store/views/delete_data_connector.py            19     10    47%
apps/store/views/detail_data_connectors.py           42      0   100%
apps/store/views/list_data_connectors.py             50     32    36%
apps/store/views/reset_data_connector.py             18      9    50%
apps/store/views/store.py                            15      8    47%
apps/store/views/sync_data_connector.py              18      9    50%
apps/user/admin.py                                   29      7    76%
apps/user/apps.py                                     5      0   100%
apps/user/forms.py                                   14      1    93%
apps/user/models.py                                  72     13    82%
apps/user/serializers.py                             88     31    65%
apps/user/signals.py                                 11      4    64%
apps/user/tasks.py                                   15      6    60%
apps/user/urls.py                                     7      0   100%
apps/user/utils.py                                    4      0   100%
apps/user/validators.py                               7      2    71%
apps/user/views.py                                   61     26    57%
manage.py                                            13      6    54%
---------------------------------------------------------------------
TOTAL                                              2250    879    61%

.gitlab-ci.yml 文件

我的实际文件要大得多。但这里是相关部分:

services:
    - docker:20.10.7-dind
    - postgres:latest

stages:
    - test


variables:
    POSTGRES_DB: test_mia
    POSTGRES_USER: mia_dev
    POSTGRES_PASSWORD: test123

build_test_db:
    image: postgres
    script:
        - export PGPASSWORD=$POSTGRES_PASSWORD
        - psql -h "postgres" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT 'OK' AS status;"

test:
    image: python:3.8-slim-buster
    stage: test
    before_script:
        - |
            apt-get update && apt-get install -y --no-install-recommends \
            gcc \
            musl-dev \
            python3-dev \
            libpq-dev \
            libgnutls28-dev \
            git \
            && rm -rf /var/lib/apt/lists/*
        - python -m pip install --upgrade pip
        - pip install uwsgi
        - pip install -r requirements.txt
        - echo $TEST_SECRETS | base64 -d > config/settings/local.py
    script:
        - echo RUNNING UNIT TESTS
        - coverage run manage.py test -v 2 && coverage report
    needs:
        - ["build_test_db"]
    environment: test
    # only:
    #     - develop


gitlab-ci.yml 的脚本部分的 coverage run manage.py test ... 之前添加 python manage.py migrate 行。