146 lines
5.2 KiB
Python
146 lines
5.2 KiB
Python
"""
|
||
共享 Excel 工具单元测试(excel_utils)
|
||
=============================================================
|
||
|
||
验证通用 Excel 导入/导出机制(供职级/用户等模块复用):
|
||
- parse_excel_rows:按列定义读取,必填/解析错误逐行收集,空行跳过
|
||
- parse_int / parse_status / format_status:单元格解析与状态展示格式化
|
||
- build_export_rows:按列 key 取值并应用 formatter
|
||
- save_export_file:写工作簿、生成访问 URL(路径通过 monkeypatch 指向临时目录)
|
||
|
||
不依赖具体业务模块,列定义在用例内自构造。
|
||
"""
|
||
|
||
import pytest
|
||
from openpyxl.reader.excel import load_workbook
|
||
from openpyxl.workbook import Workbook
|
||
|
||
from src.common.utils import excel_utils
|
||
|
||
|
||
# 用例自构造的列定义(结构与 level/user 模块一致)
|
||
COLUMNS = [
|
||
{"title": "名称", "index": 1, "key": "name", "required": True},
|
||
{"title": "排序", "index": 2, "key": "sort", "parser": excel_utils.parse_int},
|
||
{"title": "状态", "index": 3, "key": "status", "parser": excel_utils.parse_status},
|
||
]
|
||
|
||
|
||
class TestParseStatus:
|
||
"""状态单元格解析:中文 / 数字 -> 1/2,非法值报错"""
|
||
|
||
def test_normal_and_disabled(self):
|
||
assert excel_utils.parse_status("正常") == 1
|
||
assert excel_utils.parse_status("禁用") == 2
|
||
assert excel_utils.parse_status("1") == 1
|
||
assert excel_utils.parse_status(2) == 2
|
||
|
||
def test_invalid_raises(self):
|
||
with pytest.raises(ValueError):
|
||
excel_utils.parse_status("未知")
|
||
|
||
|
||
class TestParseInt:
|
||
"""整数单元格解析"""
|
||
|
||
def test_valid(self):
|
||
assert excel_utils.parse_int("10") == 10
|
||
assert excel_utils.parse_int(3) == 3
|
||
|
||
def test_invalid_raises(self):
|
||
with pytest.raises(ValueError):
|
||
excel_utils.parse_int("abc")
|
||
|
||
|
||
class TestFormatStatus:
|
||
"""状态展示格式化:1/2 -> 正常/禁用,未知值原样返回"""
|
||
|
||
def test_mapping(self):
|
||
assert excel_utils.format_status(1) == "正常"
|
||
assert excel_utils.format_status(2) == "禁用"
|
||
|
||
def test_unknown_passthrough(self):
|
||
assert excel_utils.format_status(0) == 0
|
||
|
||
|
||
class TestBuildExportRows:
|
||
"""导出行构建:按列 key 取值 + formatter 展示格式化"""
|
||
|
||
def test_formatter_applied_and_key_lookup(self):
|
||
records = [
|
||
{"name": "初级", "status": 1, "sort": 2},
|
||
{"name": "禁用级", "status": 2, "sort": 1},
|
||
]
|
||
columns = [
|
||
{"title": "名称", "key": "name"},
|
||
{"title": "排序", "key": "sort"},
|
||
{"title": "状态", "key": "status", "formatter": excel_utils.format_status},
|
||
]
|
||
assert excel_utils.build_export_rows(records, columns) == [
|
||
["初级", 2, "正常"],
|
||
["禁用级", 1, "禁用"],
|
||
]
|
||
|
||
def test_missing_key_becomes_none(self):
|
||
columns = [{"title": "缺失键", "key": "nope"}]
|
||
assert excel_utils.build_export_rows([{"name": "x"}], columns) == [[None]]
|
||
|
||
|
||
class TestParseExcelRows:
|
||
"""导入行解析:必填校验 / parser 错误 / 空行跳过 / 可选空单元格"""
|
||
|
||
def _make_wb(self, path):
|
||
wb = Workbook()
|
||
ws = wb.worksheets[0]
|
||
ws.append(["名称", "排序", "状态"]) # 表头(start_row=2 跳过)
|
||
ws.append(["初级", 1, "正常"]) # 合法
|
||
ws.append(["坏排序", "abc", "正常"]) # 排序解析失败
|
||
ws.append(["缺状态", 2, ""]) # 可选空单元格 -> None
|
||
ws.append([None, None, None]) # 整行为空 -> 跳过
|
||
ws.append(["", 3, "禁用"]) # 必填缺失
|
||
wb.save(path)
|
||
|
||
def test_rows_and_errors(self, tmp_path):
|
||
path = tmp_path / "in.xlsx"
|
||
self._make_wb(path)
|
||
|
||
rows, errors = excel_utils.parse_excel_rows(str(path), COLUMNS)
|
||
|
||
# 合法行 + 可选空单元格行保留;错误行 / 空行被剔除
|
||
assert rows == [
|
||
{"name": "初级", "sort": 1, "status": 1},
|
||
{"name": "缺状态", "sort": 2, "status": None},
|
||
]
|
||
# 行级错误带行号与原因
|
||
assert any("第3行 排序:" in e for e in errors)
|
||
assert any("第6行 名称不能为空" in e for e in errors)
|
||
|
||
|
||
class TestSaveExportFile:
|
||
"""导出文件落盘与 URL 生成"""
|
||
|
||
def test_writes_file_and_returns_url(self, tmp_path, monkeypatch):
|
||
monkeypatch.setattr(excel_utils, "FLASK_TEMP_PATH", str(tmp_path))
|
||
monkeypatch.setattr(excel_utils, "get_file_url", lambda url: "/static" + url)
|
||
|
||
headers = ["名称", "状态"]
|
||
rows = [["初级", "正常"], ["高级", "禁用"]]
|
||
url = excel_utils.save_export_file(headers, rows, filename_prefix="level")
|
||
|
||
# URL 由 get_file_url 拼接(返回约定前缀 + 路径)
|
||
assert url.startswith("/static")
|
||
|
||
# 文件真实落盘到临时目录,表头与数据一致
|
||
import glob
|
||
|
||
saved = glob.glob(str(tmp_path) + "/**/*.xlsx", recursive=True)
|
||
assert len(saved) == 1
|
||
wb = load_workbook(saved[0])
|
||
ws = wb.worksheets[0]
|
||
assert list(ws.iter_rows(values_only=True)) == [
|
||
("名称", "状态"),
|
||
("初级", "正常"),
|
||
("高级", "禁用"),
|
||
]
|
||
wb.close()
|