feapder/tests/test_csv_pipeline/test_functionality.py

455 lines
14 KiB
Python
Raw Permalink 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.

# -*- coding: utf-8 -*-
"""
CSV Pipeline 功能测试
测试内容:
1. 基础功能测试
2. 异常处理测试
3. 边界条件测试
4. 兼容性测试
Created on 2025-10-16
@author: 道长
@email: ctrlf4@yeah.net
"""
import csv
import os
import sys
import shutil
from pathlib import Path
# 添加项目路径
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from feapder.pipelines.csv_pipeline import CsvPipeline
class FunctionalityTester:
"""CSV Pipeline 功能测试器"""
def __init__(self, test_dir="test_output"):
"""初始化测试器"""
self.test_dir = test_dir
self.pipeline = None
self.passed = 0
self.failed = 0
def setup(self):
"""测试前准备"""
if os.path.exists(self.test_dir):
shutil.rmtree(self.test_dir)
os.makedirs(self.test_dir, exist_ok=True)
csv_dir = os.path.join(self.test_dir, "csv")
self.pipeline = CsvPipeline(csv_dir=csv_dir)
print(f"✅ 测试环境准备完成")
def teardown(self):
"""测试后清理"""
if self.pipeline:
self.pipeline.close()
def assert_true(self, condition, message):
"""断言真"""
if condition:
print(f"{message}")
self.passed += 1
else:
print(f"{message}")
self.failed += 1
def assert_false(self, condition, message):
"""断言假"""
self.assert_true(not condition, message)
def assert_equal(self, actual, expected, message):
"""断言相等"""
if actual == expected:
print(f"{message}")
self.passed += 1
else:
print(f"{message} (期望: {expected}, 实际: {actual})")
self.failed += 1
def test_basic_save(self):
"""测试基础保存功能"""
print("\n" + "=" * 80)
print("测试 1: 基础保存功能")
print("=" * 80)
# 测试保存单条数据
item = {"id": 1, "name": "Test Product", "price": 99.99}
result = self.pipeline.save_items("product", [item])
self.assert_true(result, "保存单条数据")
# 检查文件是否创建
csv_file = os.path.join(self.pipeline.csv_dir, "product.csv")
self.assert_true(os.path.exists(csv_file), "CSV 文件已创建")
# 检查数据是否正确
with open(csv_file, 'r', encoding='utf-8', newline='') as f:
reader = csv.DictReader(f)
rows = list(reader)
self.assert_equal(len(rows), 1, "文件中有 1 条数据")
if rows:
self.assert_equal(rows[0]["id"], "1", "数据 ID 正确")
self.assert_equal(rows[0]["name"], "Test Product", "数据名称正确")
def test_batch_save(self):
"""测试批量保存"""
print("\n" + "=" * 80)
print("测试 2: 批量保存功能")
print("=" * 80)
# 生成测试数据
items = []
for i in range(10):
items.append({
"id": i + 1,
"name": f"Product_{i + 1}",
"price": 100 + i,
})
result = self.pipeline.save_items("batch_test", items)
self.assert_true(result, "批量保存 10 条数据")
# 检查数据行数
csv_file = os.path.join(self.pipeline.csv_dir, "batch_test.csv")
with open(csv_file, 'r', encoding='utf-8', newline='') as f:
reader = csv.DictReader(f)
rows = list(reader)
self.assert_equal(len(rows), 10, "批量保存数据行数正确")
def test_empty_items(self):
"""测试空数据处理"""
print("\n" + "=" * 80)
print("测试 3: 空数据处理")
print("=" * 80)
result = self.pipeline.save_items("empty_test", [])
self.assert_true(result, "空数据列表返回 True")
def test_special_characters(self):
"""测试特殊字符处理"""
print("\n" + "=" * 80)
print("测试 4: 特殊字符处理")
print("=" * 80)
items = [
{
"id": 1,
"name": "产品名称",
"description": 'Contains "quotes" and, commas',
"emoji": "😀🎉🚀",
"newline": "Line1\nLine2",
}
]
result = self.pipeline.save_items("special_chars", items)
self.assert_true(result, "保存包含特殊字符的数据")
# 读取并检查
csv_file = os.path.join(self.pipeline.csv_dir, "special_chars.csv")
with open(csv_file, 'r', encoding='utf-8', newline='') as f:
reader = csv.DictReader(f)
rows = list(reader)
if rows:
self.assert_equal(rows[0]["name"], "产品名称", "中文字符正确")
self.assert_equal(
rows[0].get("emoji", ""),
"😀🎉🚀",
"Emoji 正确"
)
def test_multiple_tables(self):
"""测试多表存储"""
print("\n" + "=" * 80)
print("测试 5: 多表存储")
print("=" * 80)
tables = ["product", "user", "order"]
for table in tables:
item = {"id": 1, "name": f"Test {table}"}
result = self.pipeline.save_items(table, [item])
self.assert_true(result, f"保存到表 {table}")
# 检查所有文件
for table in tables:
csv_file = os.path.join(self.pipeline.csv_dir, f"{table}.csv")
self.assert_true(os.path.exists(csv_file), f"{table} 的 CSV 文件存在")
def test_header_only_once(self):
"""测试表头只写一次"""
print("\n" + "=" * 80)
print("测试 6: 表头只写一次")
print("=" * 80)
table = "header_test"
# 第一次写入
items1 = [{"id": 1, "name": "Product 1"}]
self.pipeline.save_items(table, items1)
# 第二次写入
items2 = [{"id": 2, "name": "Product 2"}]
self.pipeline.save_items(table, items2)
# 检查表头行数
csv_file = os.path.join(self.pipeline.csv_dir, f"{table}.csv")
with open(csv_file, 'r', encoding='utf-8', newline='') as f:
lines = f.readlines()
# 应该是1 个表头 + 2 条数据
self.assert_equal(len(lines), 3, "文件中只有 1 行表头和 2 行数据")
def test_numeric_values(self):
"""测试数值类型"""
print("\n" + "=" * 80)
print("测试 7: 数值类型处理")
print("=" * 80)
items = [
{
"id": 1,
"price": 99.99,
"stock": 100,
"rating": 4.5,
"active": True,
}
]
result = self.pipeline.save_items("numeric_test", items)
self.assert_true(result, "保存包含各类数值的数据")
# 读取并检查
csv_file = os.path.join(self.pipeline.csv_dir, "numeric_test.csv")
with open(csv_file, 'r', encoding='utf-8', newline='') as f:
reader = csv.DictReader(f)
rows = list(reader)
if rows:
self.assert_equal(rows[0]["price"], "99.99", "浮点数正确")
self.assert_equal(rows[0]["stock"], "100", "整数正确")
self.assert_equal(rows[0]["rating"], "4.5", "小数正确")
def test_large_values(self):
"""测试大值处理"""
print("\n" + "=" * 80)
print("测试 8: 大值处理")
print("=" * 80)
large_text = "x" * 10000 # 10KB 的文本
items = [
{
"id": 1,
"name": "Large Content",
"content": large_text,
}
]
result = self.pipeline.save_items("large_test", items)
self.assert_true(result, "保存大内容数据")
# 检查数据完整性
csv_file = os.path.join(self.pipeline.csv_dir, "large_test.csv")
with open(csv_file, 'r', encoding='utf-8', newline='') as f:
reader = csv.DictReader(f)
rows = list(reader)
if rows:
self.assert_equal(
len(rows[0]["content"]),
len(large_text),
"大内容数据完整"
)
def test_update_items_fallback(self):
"""测试 update_items 降级为 save"""
print("\n" + "=" * 80)
print("测试 9: update_items 降级为 save")
print("=" * 80)
items = [{"id": 1, "name": "Product 1", "price": 100}]
result = self.pipeline.update_items("update_test", items, ("price",))
self.assert_true(result, "update_items 返回 True")
# 检查数据是否存在
csv_file = os.path.join(self.pipeline.csv_dir, "update_test.csv")
self.assert_true(os.path.exists(csv_file), "update_items 创建了 CSV 文件")
def test_file_operations(self):
"""测试文件操作"""
print("\n" + "=" * 80)
print("测试 10: 文件操作")
print("=" * 80)
items = [{"id": 1, "name": "Test"}]
table = "file_test"
result = self.pipeline.save_items(table, items)
self.assert_true(result, "保存数据")
csv_file = os.path.join(self.pipeline.csv_dir, f"{table}.csv")
# 检查文件是否可读
try:
with open(csv_file, 'r', encoding='utf-8') as f:
f.read()
self.assert_true(True, "CSV 文件可读")
except Exception as e:
self.assert_true(False, f"CSV 文件可读 ({e})")
# 检查文件大小
file_size = os.path.getsize(csv_file)
self.assert_true(file_size > 0, f"CSV 文件大小 > 0 ({file_size} 字节)")
def test_concurrent_same_table(self):
"""测试同表并发写入"""
print("\n" + "=" * 80)
print("测试 11: 同表并发写入Per-Table Lock")
print("=" * 80)
import threading
table = "concurrent_same_table"
errors = []
def write_data(thread_id):
try:
items = [{"id": thread_id, "name": f"Item_{thread_id}"}]
result = self.pipeline.save_items(table, items)
if not result:
errors.append(f"线程{thread_id}写入失败")
except Exception as e:
errors.append(f"线程{thread_id}异常: {e}")
# 创建多个线程
threads = []
for i in range(5):
t = threading.Thread(target=write_data, args=(i,))
t.start()
threads.append(t)
# 等待所有线程完成
for t in threads:
t.join()
self.assert_equal(len(errors), 0, "并发写入无错误")
# 检查数据完整性
csv_file = os.path.join(self.pipeline.csv_dir, f"{table}.csv")
with open(csv_file, 'r', encoding='utf-8', newline='') as f:
reader = csv.DictReader(f)
rows = list(reader)
self.assert_true(len(rows) > 0, "并发写入产生了数据")
def test_directory_creation(self):
"""测试目录自动创建"""
print("\n" + "=" * 80)
print("测试 12: 目录自动创建")
print("=" * 80)
# 创建新的 pipeline 实例,指定不存在的目录
new_csv_dir = os.path.join(self.test_dir, "new_csv_dir")
self.assert_false(os.path.exists(new_csv_dir), "新目录不存在")
new_pipeline = CsvPipeline(csv_dir=new_csv_dir)
self.assert_true(os.path.exists(new_csv_dir), "目录自动创建")
new_pipeline.close()
def test_none_values(self):
"""测试 None 值处理"""
print("\n" + "=" * 80)
print("测试 13: None 值处理")
print("=" * 80)
items = [
{
"id": 1,
"name": "Product",
"description": None,
"optional_field": "",
}
]
result = self.pipeline.save_items("none_test", items)
self.assert_true(result, "保存包含 None 值的数据")
# 检查文件
csv_file = os.path.join(self.pipeline.csv_dir, "none_test.csv")
with open(csv_file, 'r', encoding='utf-8', newline='') as f:
reader = csv.DictReader(f)
rows = list(reader)
if rows:
# None 会被转换为字符串 "None"
self.assert_true("None" in rows[0]["description"],
"None 值被正确处理")
def run_all_tests(self):
"""运行所有测试"""
print("\n")
print("" + "" * 78 + "")
print("" + " CSV Pipeline 功能测试 ".center(78) + "")
print("" + " 作者: 道长 | 日期: 2025-10-16 ".center(78) + "")
print("" + "" * 78 + "")
try:
self.setup()
# 运行所有测试
self.test_basic_save()
self.test_batch_save()
self.test_empty_items()
self.test_special_characters()
self.test_multiple_tables()
self.test_header_only_once()
self.test_numeric_values()
self.test_large_values()
self.test_update_items_fallback()
self.test_file_operations()
self.test_concurrent_same_table()
self.test_directory_creation()
self.test_none_values()
# 打印总结
self.print_summary()
return self.failed == 0
except Exception as e:
print(f"\n❌ 测试过程中出错: {e}")
import traceback
traceback.print_exc()
return False
finally:
self.teardown()
def print_summary(self):
"""打印测试总结"""
print("\n" + "=" * 80)
print("测试总结")
print("=" * 80)
print(f"✅ 通过: {self.passed}")
print(f"❌ 失败: {self.failed}")
print(f"总计: {self.passed + self.failed}")
if self.failed == 0:
print("\n🎉 所有测试通过!")
else:
print(f"\n⚠️ 有 {self.failed} 个测试失败")
print("=" * 80)
def main():
"""主函数"""
tester = FunctionalityTester(test_dir="tests/test_csv_pipeline/test_output_func")
success = tester.run_all_tests()
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())