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:
co-authored by
Claude
parent
a6e094378c
commit
0cfaf8c894
+11
-2
@@ -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
|
||||
|
||||
@@ -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]
|
||||
Reference in New Issue
Block a user