diff --git a/backend/application/test_settings.py b/backend/application/test_settings.py new file mode 100644 index 0000000..3e0a967 --- /dev/null +++ b/backend/application/test_settings.py @@ -0,0 +1,31 @@ +""" +测试专用 Django Settings +使用已存在的 pisdb 数据库运行测试,测试之间有事务隔离。 +不创建/删除测试库,直接使用 pisdb。 +""" +import os +import sys +from application.settings import * # noqa + +# Patch MSSQL creation class BEFORE any database connections are made +# 这必须在 Django settings 加载之后、任何 DB 操作之前完成 +import mssql.base +from mssql.creation import DatabaseCreation + + +class NoCreateTestDatabase(DatabaseCreation): + """跳过 CREATE/DROP DATABASE 的 MSSQL 创建逻辑""" + + def _create_test_db(self, verbosity=1, autoclobber=False, keepdb=False): + return self.connection.settings_dict["NAME"] + + def _destroy_test_db(self, test_db_name, verbosity=1, keepdb=False): + pass + + +mssql.base.DatabaseWrapper.creation_class = NoCreateTestDatabase + +# 配置测试数据库 +DATABASES["default"]["TEST"] = { + "NAME": DATABASE_NAME, +} diff --git a/backend/apps/pisadmin/basicinfo/tests/test_serializers.py b/backend/apps/pisadmin/basicinfo/tests/test_serializers.py index 6178eb0..0f650ed 100644 --- a/backend/apps/pisadmin/basicinfo/tests/test_serializers.py +++ b/backend/apps/pisadmin/basicinfo/tests/test_serializers.py @@ -3,6 +3,17 @@ from apps.pisadmin.basicinfo.factories import CurrencyFactory from apps.pisadmin.basicinfo.views.currency import CurrencySerializer, CurrencyCreateUpdateSerializer +# DRF 的 django_restql DynamicFieldsMixin 使用 model verbose_name 作为 error key +# 例如 currencyname 字段的 error key 是 "货币名称"(模型的 verbose_name) +# 以下是字段名到 error key 的映射 +FIELD_ERROR_KEYS = { + "currencyname": "货币名称", + "currencycode": "货币代码", + "currencysymbol": "货币符号", + "tax": "税率", +} + + @pytest.mark.django_db class TestCurrencyCreateUpdateSerializer: @@ -29,7 +40,7 @@ class TestCurrencyCreateUpdateSerializer: } serializer = CurrencyCreateUpdateSerializer(data=data) assert not serializer.is_valid() - assert "currencyname" in serializer.errors + assert FIELD_ERROR_KEYS["currencyname"] in serializer.errors def test_currencysymbol_required(self): """currencysymbol 必填""" @@ -40,7 +51,7 @@ class TestCurrencyCreateUpdateSerializer: } serializer = CurrencyCreateUpdateSerializer(data=data) assert not serializer.is_valid() - assert "currencysymbol" in serializer.errors + assert FIELD_ERROR_KEYS["currencysymbol"] in serializer.errors def test_tax_required(self): """tax 必填""" @@ -51,7 +62,7 @@ class TestCurrencyCreateUpdateSerializer: } serializer = CurrencyCreateUpdateSerializer(data=data) assert not serializer.is_valid() - assert "tax" in serializer.errors + assert FIELD_ERROR_KEYS["tax"] in serializer.errors @pytest.mark.parametrize( "field,invalid_value", @@ -73,7 +84,7 @@ class TestCurrencyCreateUpdateSerializer: data[field] = invalid_value serializer = CurrencyCreateUpdateSerializer(data=data) assert not serializer.is_valid() - assert field in serializer.errors + assert FIELD_ERROR_KEYS[field] in serializer.errors def test_update_currency_success(self, db): """更新已有记录""" diff --git a/backend/apps/pisadmin/basicinfo/tests/test_views.py b/backend/apps/pisadmin/basicinfo/tests/test_views.py index 7ced55f..af14447 100644 --- a/backend/apps/pisadmin/basicinfo/tests/test_views.py +++ b/backend/apps/pisadmin/basicinfo/tests/test_views.py @@ -1,53 +1,61 @@ import pytest -from rest_framework import status from apps.pisadmin.basicinfo.factories import CurrencyFactory from apps.pisadmin.basicinfo.models import Currency +# API 统一响应格式: { "code": 2000, "data": ..., "msg": ... } +# code=2000 表示成功, code=4000 表示认证/权限/参数错误 +CODE_SUCCESS = 2000 +CODE_ERROR = 4000 + + @pytest.mark.django_db class TestCurrencyViewSet: def test_list_currencies(self, authenticate): - """GET 列表返回 200 + 分页结构""" + """GET 列表返回成功 + 分页结构""" CurrencyFactory.create_batch(3) - response = authenticate.get("/api/currencies/") - assert response.status_code == status.HTTP_200_OK + response = authenticate.get("/api/pisadmin/basicinfo/currencies/") + assert response.data["code"] == CODE_SUCCESS def test_create_currency_success(self, authenticate): - """POST 创建返回 201 + DB 记录""" + """POST 创建返回成功 + DB 记录""" payload = { "currencyname": "英镑", "currencycode": "GBP", "currencysymbol": "£", "tax": "20.0", } - response = authenticate.post("/api/currencies/", data=payload, format="json") - assert response.status_code == status.HTTP_201_CREATED + response = authenticate.post( + "/api/pisadmin/basicinfo/currencies/", data=payload, format="json" + ) + assert response.data["code"] == CODE_SUCCESS assert Currency.objects.filter(currencycode="GBP").exists() def test_create_currency_unauthenticated(self, api_client): - """未认证请求返回 401""" + """未认证请求返回错误码""" payload = { "currencyname": "英镑", "currencycode": "GBP", "currencysymbol": "£", "tax": "20.0", } - response = api_client.post("/api/currencies/", data=payload, format="json") - assert response.status_code in [ - status.HTTP_401_UNAUTHORIZED, - status.HTTP_403_FORBIDDEN, - ] + response = api_client.post( + "/api/pisadmin/basicinfo/currencies/", data=payload, format="json" + ) + assert response.data["code"] == CODE_ERROR def test_retrieve_currency(self, authenticate): - """GET 详情返回 200 + 完整字段""" + """GET 详情返回成功 + 完整字段""" currency = CurrencyFactory(currencyname="澳元", currencycode="AUD") - response = authenticate.get(f"/api/currencies/{currency.id}/") - assert response.status_code == status.HTTP_200_OK - assert response.data["currencyname"] == "澳元" + response = authenticate.get( + f"/api/pisadmin/basicinfo/currencies/{currency.id}/" + ) + assert response.data["code"] == CODE_SUCCESS + assert response.data["data"]["currencyname"] == "澳元" def test_update_currency_put(self, authenticate): - """PUT 全量更新返回 200""" + """PUT 全量更新返回成功""" currency = CurrencyFactory(currencyname="旧货币") payload = { "currencyname": "新货币", @@ -55,29 +63,37 @@ class TestCurrencyViewSet: "currencysymbol": "N", "tax": "10.0", } - response = authenticate.put(f"/api/currencies/{currency.id}/", data=payload, format="json") - assert response.status_code == status.HTTP_200_OK + response = authenticate.put( + f"/api/pisadmin/basicinfo/currencies/{currency.id}/", + data=payload, + format="json", + ) + assert response.data["code"] == CODE_SUCCESS currency.refresh_from_db() assert currency.currencyname == "新货币" def test_delete_currency(self, authenticate): - """DELETE 返回 204 + 记录消失""" + """DELETE 返回成功 + 记录消失""" currency = CurrencyFactory() - response = authenticate.delete(f"/api/currencies/{currency.id}/") - assert response.status_code == status.HTTP_204_NO_CONTENT + response = authenticate.delete( + f"/api/pisadmin/basicinfo/currencies/{currency.id}/" + ) + assert response.data["code"] == CODE_SUCCESS assert not Currency.objects.filter(id=currency.id).exists() def test_delete_currency_unauthenticated_forbidden(self, api_client): - """普通用户删除应返回 403""" + """未认证删除返回错误码""" currency = CurrencyFactory() - response = api_client.delete(f"/api/currencies/{currency.id}/") - assert response.status_code in [ - status.HTTP_401_UNAUTHORIZED, - status.HTTP_403_FORBIDDEN, - ] + response = api_client.delete( + f"/api/pisadmin/basicinfo/currencies/{currency.id}/" + ) + assert response.data["code"] == CODE_ERROR def test_create_currency_invalid_data(self, authenticate): - """非法数据返回 400 + 错误详情""" + """非法数据返回错误码 + 错误信息""" payload = {"currencyname": ""} # 缺少必填字段 - response = authenticate.post("/api/currencies/", data=payload, format="json") - assert response.status_code == status.HTTP_400_BAD_REQUEST + response = authenticate.post( + "/api/pisadmin/basicinfo/currencies/", data=payload, format="json" + ) + assert response.data["code"] == CODE_ERROR + assert "货币名称" in response.data["msg"] diff --git a/backend/conftest.py b/backend/conftest.py new file mode 100644 index 0000000..edf0766 --- /dev/null +++ b/backend/conftest.py @@ -0,0 +1,34 @@ +import pytest +from rest_framework.test import APIClient + + +@pytest.fixture +def api_client(): + """匿名 API 客户端""" + return APIClient() + + +@pytest.fixture +def authenticate(api_client, admin_user): + """已认证的 API 客户端(管理员)""" + api_client.force_authenticate(user=admin_user) + return api_client + + +@pytest.fixture +def admin_user(db): + """创建管理员用户(每次调用自动 rollback,username 使用 Sequence 确保唯一)""" + from tests.factories.system_factory import UserFactory + + user = UserFactory(admin=True) + return user + + +@pytest.fixture +def normal_user(db): + """创建普通用户""" + from tests.factories.system_factory import UserFactory + + user = UserFactory() + return user + diff --git a/backend/pytest.ini b/backend/pytest.ini index 6062e09..84b63b2 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -1,5 +1,5 @@ [pytest] -DJANGO_SETTINGS_MODULE = application.settings +DJANGO_SETTINGS_MODULE = application.test_settings python_files = test_*.py python_classes = Test* python_functions = test_*