refactor: P4b position 标杆模块服务层改造 + 单元测试

【服务层新范式】system/position 作为标杆:
- service 函数改「入参数据/出参数据」:get_position_page(params)->dict、
  add_position(data)/update_position(data)/update_position_status(data)->None、
  delete_positions(ids)->int
- 业务失败 raise BizError(不再 return R.failed),由全局处理器转 R.failed
- get_info 委托 PositionRepository(新增 repository.py,继承 BaseRepository)
- router 负责取参(query args / parse_request_body)与 R 包装

【单元测试】tests/unit/test_modules/test_position_service.py:
- 纯函数(_build_page_data)、BizError 契约(空数据/空ID/无效ID)、
  mock 批量删除(ID 解析验证)——无需数据库即可测试
- conftest flask_app fixture 增加 SECRET_KEY + 关闭 CSRF

【验收】12 passed(5 响应 + 7 position);应用启动;flake8 零告警;
position 路由 401 拦截正常。服务层测试化范式验证可行

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DjangoAdmin Dev
2026-08-07 14:42:03 +08:00
co-authored by Claude
parent a6e094378c
commit 0cfaf8c894
6 changed files with 204 additions and 130 deletions
+11 -2
View File
@@ -27,5 +27,14 @@ if ROOT not in sys.path:
@pytest.fixture
def flask_app():
"""最小 Flask 应用实例(无数据库/中间件,仅提供应用上下文)。"""
return Flask(__name__)
"""
最小 Flask 应用实例(无数据库/中间件,仅提供应用上下文)。
配置项说明:
- SECRET_KEY: FlaskForm CSRF 校验需要
- WTF_CSRF_ENABLED=False: 单元测试关闭 CSRF
"""
app = Flask(__name__)
app.config['SECRET_KEY'] = 'test-secret-key'
app.config['WTF_CSRF_ENABLED'] = False
return app
+1
View File
@@ -0,0 +1 @@
"""业务模块服务单元测试。"""
@@ -0,0 +1,81 @@
"""
岗位服务单元测试(P4 服务层改造标杆)
=============================================================
验证 P4 改造后的服务层契约:
- 服务函数「入参数据、出参数据」
- 业务失败通过 raise BizError 表达(不再 return R.failed)
- 纯函数与可 mock 路径无需数据库即可测试
"""
import pytest
from src.core.exceptions import BizError
from src.modules.system.position import service as position_service
class TestBuildPageData:
"""分页数据构建(纯函数)"""
def test_shape(self):
data = position_service._build_page_data([], 0, 10, 1, 0)
assert data == {'records': [], 'total': 0, 'size': 10, 'current': 1, 'pages': 0}
class TestAddPosition:
"""添加岗位"""
def test_empty_data_raises_bizerror(self, flask_app):
"""空数据触发表单验证失败 → BizError(FlaskForm 需应用上下文)"""
with flask_app.app_context():
with pytest.raises(BizError):
position_service.add_position({})
class TestUpdatePosition:
"""更新岗位"""
def test_empty_data_raises_bizerror(self, flask_app):
with flask_app.app_context():
with pytest.raises(BizError):
position_service.update_position({})
class TestUpdateStatus:
"""状态设置"""
def test_empty_data_raises_bizerror(self, flask_app):
with flask_app.app_context():
with pytest.raises(BizError):
position_service.update_position_status({})
class TestDeletePositions:
"""批量删除"""
def test_empty_raises(self):
"""空 ID 触发 BizError"""
with pytest.raises(BizError, match="记录ID不存在"):
position_service.delete_positions('')
def test_invalid_ids_raises(self):
"""非数字 ID 解析为空 → BizError"""
with pytest.raises(BizError, match="无效的记录ID"):
position_service.delete_positions('abc')
def test_parse_and_delete(self, monkeypatch):
"""ID 解析后调用模型批量删除,返回删除计数"""
from src.modules.system.position.models import Position
captured = {}
def fake_batch_delete(cls, ids):
captured['ids'] = ids
return len(ids)
monkeypatch.setattr(Position, 'batch_delete', classmethod(fake_batch_delete))
count = position_service.delete_positions('1,2,3')
assert count == 3
assert captured['ids'] == [1, 2, 3]