Files
DjangoAdmin_Flask_NaiveVue/tests/unit/test_common/test_excel_utils.py
T
2026-09-12 22:24:53 +08:00

146 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
共享 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()