# -*- 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())