From 8aa1a5ac91c6ac669a24376b82940df0306b8d80 Mon Sep 17 00:00:00 2001 From: liangweihao <734499798@qq.com> Date: Sat, 27 Dec 2025 16:52:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0PyQt=20GUI=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8C=96=E6=B5=8B=E8=AF=95=E5=A5=97=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增5个GUI测试模块,覆盖所有主要功能: - test_login_gui.py: 登录和密码管理测试(7个测试) - test_stock_gui.py: 库存管理测试(4个测试) - test_raw_material_gui.py: 原料管理测试(7个测试) - test_garment_gui.py: 款式管理测试(2个测试) - test_purchase_order_gui.py: 采购单生成测试(2个测试) 测试特点: - 真实GUI交互测试(填写表单、点击按钮、搜索过滤) - 业务逻辑验证(重复数据拒绝、空值验证、计算正确性) - 独立测试环境(临时数据库,自动清理) - 自动化消息框(Mock QMessageBox) 总计22个GUI测试,全部通过 ✓ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- test/README_GUI_TESTS.md | 156 ++++++++++++++++++ test/test_database.py | 261 ++++++++++++++++++++++++++++++ test/test_garment.py | 216 +++++++++++++++++++++++++ test/test_garment_gui.py | 125 ++++++++++++++ test/test_gui.py | 206 +++++++++++++++++++++++ test/test_login.py | 127 +++++++++++++++ test/test_login_gui.py | 163 +++++++++++++++++++ test/test_purchase_order.py | 111 +++++++++++++ test/test_purchase_order_gui.py | 116 +++++++++++++ test/test_raw_material.py | 265 ++++++++++++++++++++++++++++++ test/test_raw_material_gui.py | 278 ++++++++++++++++++++++++++++++++ test/test_stock.py | 272 +++++++++++++++++++++++++++++++ test/test_stock_gui.py | 161 ++++++++++++++++++ test/test_unit_conversion.py | 142 ++++++++++++++++ 14 files changed, 2599 insertions(+) create mode 100644 test/README_GUI_TESTS.md create mode 100644 test/test_database.py create mode 100644 test/test_garment.py create mode 100644 test/test_garment_gui.py create mode 100644 test/test_gui.py create mode 100644 test/test_login.py create mode 100644 test/test_login_gui.py create mode 100644 test/test_purchase_order.py create mode 100644 test/test_purchase_order_gui.py create mode 100644 test/test_raw_material.py create mode 100644 test/test_raw_material_gui.py create mode 100644 test/test_stock.py create mode 100644 test/test_stock_gui.py create mode 100644 test/test_unit_conversion.py diff --git a/test/README_GUI_TESTS.md b/test/README_GUI_TESTS.md new file mode 100644 index 0000000..8f0fe6e --- /dev/null +++ b/test/README_GUI_TESTS.md @@ -0,0 +1,156 @@ +# GUI测试文档 + +## 概述 + +本项目已为所有主要模块创建了基于PyQt的GUI集成测试,用于测试实际的用户界面交互和业务逻辑。 + +## 测试文件列表 + +### 1. test_login_gui.py - 登录对话框GUI测试 +**测试数量**: 7个测试 +**测试内容**: +- 管理员登录成功/失败 +- 普通用户登录成功/失败 +- 获取默认密码 +- 设置新密码 +- 使用新密码登录 + +### 2. test_stock_gui.py - 库存管理GUI测试 +**测试数量**: 4个测试 +**测试内容**: +- 加载原料列表 +- 搜索原料 +- 库存数量计算 +- 库存扣除消耗后的计算 + +### 3. test_raw_material_gui.py - 原料管理GUI测试 +**测试数量**: 7个测试 +**测试内容**: +- 添加基本原料 +- 添加重复型号(验证拒绝) +- 添加空型号(验证拒绝) +- 删除原料 +- 编辑原料 +- 按类目过滤 +- 按型号搜索 + +### 4. test_garment_gui.py - 款式管理GUI测试 +**测试数量**: 2个测试 +**测试内容**: +- 加载款式列表 +- 搜索款式 + +### 5. test_purchase_order_gui.py - 采购单生成GUI测试 +**测试数量**: 2个测试 +**测试内容**: +- 生成采购单 +- 材料用量计算 + +## 测试统计 + +**总测试数量**: 22个测试 +**测试状态**: ✓ 全部通过 +**测试时间**: ~14秒 + +## 运行测试 + +### 运行单个模块测试 +```bash +# 登录测试 +python test\test_login_gui.py -v + +# 库存测试 +python test\test_stock_gui.py -v + +# 原料测试 +python test\test_raw_material_gui.py -v + +# 款式测试 +python test\test_garment_gui.py -v + +# 采购单测试 +python test\test_purchase_order_gui.py -v +``` + +### 运行所有GUI测试 +```bash +cd test +python -m unittest test_login_gui.py test_stock_gui.py test_raw_material_gui.py test_garment_gui.py test_purchase_order_gui.py -v +``` + +## 测试特点 + +### 1. 真实GUI交互 +- 模拟用户填写表单 +- 模拟点击按钮 +- 模拟选择下拉框 +- 模拟输入搜索关键词 + +### 2. 业务逻辑验证 +- 验证重复数据拒绝 +- 验证空值验证 +- 验证数据计算正确性 +- 验证过滤和搜索功能 + +### 3. 独立测试环境 +- 每个测试使用临时数据库 +- 测试之间互不干扰 +- 自动清理测试数据 + +### 4. 自动化消息框 +- Mock了QMessageBox以便自动化测试 +- 不需要人工点击确认对话框 +- 可以验证警告和错误消息 + +## 测试架构 + +``` +test/ +├── test_login_gui.py # 登录对话框测试 +├── test_stock_gui.py # 库存管理测试 +├── test_raw_material_gui.py # 原料管理测试 +├── test_garment_gui.py # 款式管理测试 +├── test_purchase_order_gui.py # 采购单生成测试 +└── README_GUI_TESTS.md # 本文档 +``` + +## 依赖项 + +- Python 3.x +- PyQt5 +- sqlite3 (内置) +- unittest (内置) + +## 注意事项 + +1. 测试需要在有GUI环境的系统上运行 +2. 测试会创建临时数据库文件,测试后自动清理 +3. 所有消息框已被Mock,不会弹出实际对话框 +4. 测试使用独立的QApplication实例 + +## 扩展测试 + +如需添加新的GUI测试,请参考现有测试文件的结构: + +1. 继承 `unittest.TestCase` +2. 在 `setUpClass` 中创建 `QApplication` 实例 +3. 在 `setUp` 中创建临时数据库和对话框 +4. Mock消息框以便自动化 +5. 在 `tearDown` 中清理资源 +6. 编写具体的测试方法 + +## 测试覆盖率 + +当前GUI测试覆盖了以下核心功能: +- ✓ 用户登录和密码管理 +- ✓ 原料库管理(增删改查) +- ✓ 库存管理和计算 +- ✓ 款式管理 +- ✓ 采购单生成和计算 + +## 维护建议 + +1. 每次修改GUI代码后运行相关测试 +2. 添加新功能时同步添加GUI测试 +3. 定期运行所有测试确保系统稳定性 +4. 保持测试代码的可读性和可维护性 diff --git a/test/test_database.py b/test/test_database.py new file mode 100644 index 0000000..49a2957 --- /dev/null +++ b/test/test_database.py @@ -0,0 +1,261 @@ +""" +数据库模块测试 +""" + +import unittest +import tempfile +import os +from database import ( + DatabaseManager, get_db_connection, get_fabric_categories, + get_fabric_types_by_category, get_fabric_models_by_category_type, + get_password, update_password +) + + +class TestDatabaseManager(unittest.TestCase): + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test.db") + self.db_manager = DatabaseManager(self.db_path) + + def tearDown(self): + self.db_manager = None + import gc + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + os.rmdir(self.temp_dir) + except: + pass + + def test_init_db_creates_tables(self): + with self.db_manager.get_conn() as conn: + cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table'") + tables = [row[0] for row in cursor.fetchall()] + + self.assertIn("fabrics", tables) + self.assertIn("garments", tables) + self.assertIn("garment_materials", tables) + self.assertIn("admin_settings", tables) + self.assertIn("fabric_stock_in", tables) + self.assertIn("fabric_consumption", tables) + + def test_default_passwords_initialized(self): + admin_pwd = self.db_manager.get_setting("admin_password") + user_pwd = self.db_manager.get_setting("user_password") + + self.assertEqual(admin_pwd, "123456") + self.assertEqual(user_pwd, "123456") + + def test_get_setting_returns_none_for_missing_key(self): + result = self.db_manager.get_setting("nonexistent_key") + self.assertIsNone(result) + + def test_set_setting(self): + result = self.db_manager.set_setting("test_key", "test_value") + self.assertTrue(result) + + value = self.db_manager.get_setting("test_key") + self.assertEqual(value, "test_value") + + def test_set_setting_overwrites_existing(self): + self.db_manager.set_setting("test_key", "value1") + self.db_manager.set_setting("test_key", "value2") + + value = self.db_manager.get_setting("test_key") + self.assertEqual(value, "value2") + + +class TestFabricOperations(unittest.TestCase): + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test.db") + self.db_manager = DatabaseManager(self.db_path) + self._insert_test_fabrics() + + def tearDown(self): + self.db_manager = None + import gc + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + os.rmdir(self.temp_dir) + except: + pass + + def _insert_test_fabrics(self): + with self.db_manager.get_conn() as conn: + conn.execute(""" + INSERT INTO fabrics (model, category, fabric_type, supplier, color, unit) + VALUES ('F001', '布料', '棉布', '供应商A', '红色', '米') + """) + conn.execute(""" + INSERT INTO fabrics (model, category, fabric_type, supplier, color, unit) + VALUES ('F002', '布料', '丝绸', '供应商B', '蓝色', '码') + """) + conn.execute(""" + INSERT INTO fabrics (model, category, fabric_type, supplier, color, unit) + VALUES ('F003', '辅料', '拉链', '供应商C', '黑色', '个') + """) + conn.commit() + + def test_get_fabric_categories(self): + categories = get_fabric_categories(self.db_path) + + self.assertIn("布料", categories) + self.assertIn("辅料", categories) + self.assertIn("其他", categories) + + def test_get_fabric_types_by_category(self): + types = get_fabric_types_by_category(self.db_path, "布料") + + self.assertIn("棉布", types) + self.assertIn("丝绸", types) + self.assertNotIn("拉链", types) + + def test_get_fabric_types_by_category_empty(self): + types = get_fabric_types_by_category(self.db_path, "不存在的类目") + self.assertEqual(types, []) + + +class TestPasswordOperations(unittest.TestCase): + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test.db") + self.db_manager = DatabaseManager(self.db_path) + + def tearDown(self): + self.db_manager = None + import gc + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + os.rmdir(self.temp_dir) + except: + pass + + def test_get_password_default(self): + admin_pwd = get_password(self.db_path, "admin") + user_pwd = get_password(self.db_path, "user") + + self.assertEqual(admin_pwd, "123456") + self.assertEqual(user_pwd, "123456") + + def test_update_password(self): + result = update_password(self.db_path, "admin", "newpassword") + self.assertTrue(result) + + new_pwd = get_password(self.db_path, "admin") + self.assertEqual(new_pwd, "newpassword") + + +class TestStockOperations(unittest.TestCase): + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test.db") + self.db_manager = DatabaseManager(self.db_path) + self._setup_test_data() + + def tearDown(self): + self.db_manager = None + import gc + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + os.rmdir(self.temp_dir) + except: + pass + + def _setup_test_data(self): + with self.db_manager.get_conn() as conn: + conn.execute(""" + INSERT INTO fabrics (model, category, unit) + VALUES ('F001', '布料', '米') + """) + conn.execute(""" + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date) + VALUES ('F001', 100, '米', '2024-01-01') + """) + conn.execute(""" + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date) + VALUES ('F001', 50, '米', '2024-01-02') + """) + conn.execute(""" + INSERT INTO fabric_consumption (model, consume_quantity, unit, consume_date, style_number, quantity_made, loss_rate) + VALUES ('F001', 30, '米', '2024-01-03', 'G001', 10, 0.05) + """) + conn.commit() + + def test_stock_calculation(self): + with self.db_manager.get_conn() as conn: + cursor = conn.execute(""" + SELECT current_stock FROM fabric_stock_view WHERE model = 'F001' + """) + row = cursor.fetchone() + + self.assertEqual(row[0], 120) + + +class TestGarmentOperations(unittest.TestCase): + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test.db") + self.db_manager = DatabaseManager(self.db_path) + + def tearDown(self): + self.db_manager = None + import gc + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + os.rmdir(self.temp_dir) + except: + pass + + def test_insert_garment(self): + with self.db_manager.get_conn() as conn: + conn.execute(""" + INSERT INTO garments (style_number, image_path) + VALUES ('G001', 'images/g001.jpg') + """) + conn.commit() + + cursor = conn.execute("SELECT * FROM garments WHERE style_number = 'G001'") + row = cursor.fetchone() + + self.assertIsNotNone(row) + self.assertEqual(row[0], 'G001') + + def test_insert_garment_materials(self): + with self.db_manager.get_conn() as conn: + conn.execute(""" + INSERT INTO garments (style_number) VALUES ('G001') + """) + conn.execute(""" + INSERT INTO garment_materials (style_number, category, fabric_type, model, usage_per_piece, unit) + VALUES ('G001', '布料', '棉布', 'F001', 1.5, '米') + """) + conn.commit() + + cursor = conn.execute(""" + SELECT * FROM garment_materials WHERE style_number = 'G001' + """) + row = cursor.fetchone() + + self.assertIsNotNone(row) + self.assertEqual(row[2], '布料') + self.assertEqual(row[5], 1.5) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_garment.py b/test/test_garment.py new file mode 100644 index 0000000..07225b8 --- /dev/null +++ b/test/test_garment.py @@ -0,0 +1,216 @@ +""" +服装管理模块测试 - 测试服装款式和材料用量管理 +""" + +import unittest +import os +import tempfile +import gc +from database import DatabaseManager, get_db_connection + + +class TestGarment(unittest.TestCase): + """服装管理测试类""" + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test_garment.db") + self.db_manager = DatabaseManager(self.db_path) + self.conn = None + + def tearDown(self): + if self.conn: + try: + self.conn.close() + except: + pass + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + if os.path.exists(self.temp_dir): + os.rmdir(self.temp_dir) + except: + pass + + def get_conn(self): + self.conn = get_db_connection(self.db_path) + return self.conn + + # ========== 服装款式测试 ========== + + def test_add_garment_basic(self): + """测试添加服装款式""" + with self.get_conn() as conn: + conn.execute( + "INSERT INTO garments (style_number, image_path) VALUES (?, ?)", + ("G001", None) + ) + conn.commit() + cursor = conn.execute( + "SELECT style_number FROM garments WHERE style_number = ?", + ("G001",) + ) + row = cursor.fetchone() + self.assertEqual(row[0], "G001") + + def test_add_garment_with_image(self): + """测试添加带图片的服装款式""" + with self.get_conn() as conn: + conn.execute( + "INSERT INTO garments (style_number, image_path) VALUES (?, ?)", + ("G002", "images/g002.jpg") + ) + conn.commit() + cursor = conn.execute( + "SELECT image_path FROM garments WHERE style_number = ?", + ("G002",) + ) + row = cursor.fetchone() + self.assertEqual(row[0], "images/g002.jpg") + + def test_update_garment(self): + """测试更新服装款式""" + with self.get_conn() as conn: + conn.execute( + "INSERT INTO garments (style_number, image_path) VALUES (?, ?)", + ("G003", None) + ) + conn.commit() + conn.execute( + "INSERT OR REPLACE INTO garments (style_number, image_path) VALUES (?, ?)", + ("G003", "images/g003_new.jpg") + ) + conn.commit() + cursor = conn.execute( + "SELECT image_path FROM garments WHERE style_number = ?", + ("G003",) + ) + row = cursor.fetchone() + self.assertEqual(row[0], "images/g003_new.jpg") + + def test_delete_garment(self): + """测试删除服装款式""" + with self.get_conn() as conn: + conn.execute( + "INSERT INTO garments (style_number) VALUES (?)", + ("G004",) + ) + conn.commit() + conn.execute( + "DELETE FROM garments WHERE style_number = ?", + ("G004",) + ) + conn.commit() + cursor = conn.execute( + "SELECT * FROM garments WHERE style_number = ?", + ("G004",) + ) + row = cursor.fetchone() + self.assertIsNone(row) + + # ========== 材料用量测试 ========== + + def test_add_material_basic(self): + """测试添加材料用量""" + with self.get_conn() as conn: + conn.execute( + "INSERT INTO garments (style_number) VALUES (?)", + ("G005",) + ) + conn.execute(''' + INSERT INTO garment_materials + (style_number, category, fabric_type, model, usage_per_piece, unit) + VALUES (?, ?, ?, ?, ?, ?) + ''', ("G005", "布料", "棉布", "M001", 0.5, "米")) + conn.commit() + cursor = conn.execute( + "SELECT usage_per_piece, unit FROM garment_materials WHERE style_number = ?", + ("G005",) + ) + row = cursor.fetchone() + self.assertEqual(row[0], 0.5) + self.assertEqual(row[1], "米") + + def test_add_multiple_materials(self): + """测试添加多个材料""" + with self.get_conn() as conn: + conn.execute( + "INSERT INTO garments (style_number) VALUES (?)", + ("G006",) + ) + conn.execute(''' + INSERT INTO garment_materials + (style_number, category, usage_per_piece, unit) + VALUES (?, ?, ?, ?) + ''', ("G006", "A料", 0.3, "米")) + conn.execute(''' + INSERT INTO garment_materials + (style_number, category, usage_per_piece, unit) + VALUES (?, ?, ?, ?) + ''', ("G006", "B料", 0.2, "米")) + conn.commit() + cursor = conn.execute( + "SELECT COUNT(*) FROM garment_materials WHERE style_number = ?", + ("G006",) + ) + count = cursor.fetchone()[0] + self.assertEqual(count, 2) + + def test_update_material_usage(self): + """测试更新材料用量""" + with self.get_conn() as conn: + conn.execute( + "INSERT INTO garments (style_number) VALUES (?)", + ("G007",) + ) + conn.execute(''' + INSERT INTO garment_materials + (style_number, category, usage_per_piece, unit) + VALUES (?, ?, ?, ?) + ''', ("G007", "A料", 0.5, "米")) + conn.commit() + conn.execute(''' + UPDATE garment_materials SET usage_per_piece = ? + WHERE style_number = ? AND category = ? + ''', (0.8, "G007", "A料")) + conn.commit() + cursor = conn.execute( + "SELECT usage_per_piece FROM garment_materials WHERE style_number = ? AND category = ?", + ("G007", "A料") + ) + row = cursor.fetchone() + self.assertEqual(row[0], 0.8) + + def test_delete_garment_cascade_materials(self): + """测试删除款式时材料记录处理""" + with self.get_conn() as conn: + conn.execute( + "INSERT INTO garments (style_number) VALUES (?)", + ("G008",) + ) + conn.execute(''' + INSERT INTO garment_materials + (style_number, category, usage_per_piece, unit) + VALUES (?, ?, ?, ?) + ''', ("G008", "A料", 0.5, "米")) + conn.commit() + conn.execute( + "DELETE FROM garment_materials WHERE style_number = ?", + ("G008",) + ) + conn.execute( + "DELETE FROM garments WHERE style_number = ?", + ("G008",) + ) + conn.commit() + cursor = conn.execute( + "SELECT COUNT(*) FROM garment_materials WHERE style_number = ?", + ("G008",) + ) + count = cursor.fetchone()[0] + self.assertEqual(count, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_garment_gui.py b/test/test_garment_gui.py new file mode 100644 index 0000000..04698cf --- /dev/null +++ b/test/test_garment_gui.py @@ -0,0 +1,125 @@ +""" +款式管理GUI测试模块 +使用PyQt测试框架测试款式管理功能 +""" + +import unittest +import os +import sys +import tempfile +from PyQt5.QtWidgets import QApplication, QMessageBox +from PyQt5.QtTest import QTest +from PyQt5.QtCore import Qt + +# 添加父目录到路径以导入模块 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from database import DatabaseManager +from garment_dialogs import GarmentLibraryDialog + + +class TestGarmentGUI(unittest.TestCase): + """款式管理GUI测试类""" + + @classmethod + def setUpClass(cls): + """测试类初始化:创建QApplication实例""" + cls.app = QApplication.instance() + if cls.app is None: + cls.app = QApplication(sys.argv) + + def setUp(self): + """每个测试前准备:创建临时数据库和对话框""" + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test_fabric.db") + self.db_manager = DatabaseManager(self.db_path) + + # 添加测试原料 + with self.db_manager.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, color, unit, timestamp) + VALUES (?, ?, ?, ?, datetime('now')) + ''', ("TEST-FABRIC-001", "布料", "白色", "米")) + conn.commit() + + # 创建对话框实例 + self.dialog = GarmentLibraryDialog(self.db_path) + + # 保存原始消息框 + self._original_msgbox = QMessageBox.information + self._original_warning = QMessageBox.warning + self._original_critical = QMessageBox.critical + + # Mock消息框 + QMessageBox.information = lambda *args, **kwargs: None + QMessageBox.warning = lambda *args, **kwargs: None + QMessageBox.critical = lambda *args, **kwargs: None + + def tearDown(self): + """每个测试后清理""" + # 恢复消息框 + QMessageBox.information = self._original_msgbox + QMessageBox.warning = self._original_warning + QMessageBox.critical = self._original_critical + + # 关闭对话框 + self.dialog.close() + self.dialog.deleteLater() + + # 清理数据库 + import gc + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + if os.path.exists(self.temp_dir): + os.rmdir(self.temp_dir) + except: + pass + + # ========== 款式加载测试 ========== + + def test_load_garments(self): + """测试加载款式列表""" + # 添加测试款式 + with self.dialog.get_conn() as conn: + conn.execute(''' + INSERT INTO garments (style_number) + VALUES (?) + ''', ("STYLE-001",)) + conn.commit() + + # 刷新表格 + self.dialog.load_garments() + + # 验证表格有数据 + self.assertGreater(self.dialog.garment_table.rowCount(), 0, "表格应该有数据") + + def test_search_garments(self): + """测试搜索款式""" + # 添加多个测试款式 + with self.dialog.get_conn() as conn: + conn.execute(''' + INSERT INTO garments (style_number) + VALUES (?) + ''', ("SEARCH-001",)) + conn.execute(''' + INSERT INTO garments (style_number) + VALUES (?) + ''', ("OTHER-001",)) + conn.commit() + + # 搜索特定款式 + self.dialog.search_input.setText("SEARCH") + self.dialog.load_garments() + + # 验证只显示匹配的结果 + row_count = self.dialog.garment_table.rowCount() + for row in range(row_count): + style_item = self.dialog.garment_table.item(row, 0) + if style_item: + self.assertIn("SEARCH", style_item.text(), "应该只显示匹配的款式号") + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_gui.py b/test/test_gui.py new file mode 100644 index 0000000..1189cdb --- /dev/null +++ b/test/test_gui.py @@ -0,0 +1,206 @@ +""" +GUI测试模块 - 使用pytest-qt测试PyQt5界面 +""" + +import pytest +import os +import sys +import tempfile + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from PyQt5.QtWidgets import QApplication, QMessageBox, QDialog +from PyQt5.QtCore import Qt + +from database import DatabaseManager +from login_dialog import LoginDialog +from main import FabricManager + + +@pytest.fixture(scope="session") +def qapp(): + app = QApplication.instance() + if app is None: + app = QApplication([]) + yield app + + +@pytest.fixture +def temp_db(): + temp_dir = tempfile.mkdtemp() + db_path = os.path.join(temp_dir, "test_gui.db") + db_manager = DatabaseManager(db_path) + yield db_path + import gc + gc.collect() + try: + if os.path.exists(db_path): + os.remove(db_path) + os.rmdir(temp_dir) + except: + pass + + +class TestLoginDialog: + """登录对话框GUI测试""" + + def test_login_dialog_init(self, qapp, temp_db, qtbot): + dialog = LoginDialog(temp_db) + qtbot.addWidget(dialog) + + assert dialog.windowTitle() == "选择模式并登录" + assert dialog.is_admin == False + + def test_login_dialog_has_inputs(self, qapp, temp_db, qtbot): + dialog = LoginDialog(temp_db) + qtbot.addWidget(dialog) + + assert hasattr(dialog, 'admin_input') + assert hasattr(dialog, 'user_input') + + def test_admin_login_correct_password(self, qapp, temp_db, qtbot, monkeypatch): + dialog = LoginDialog(temp_db) + qtbot.addWidget(dialog) + + dialog.admin_input.setText("123456") + monkeypatch.setattr(QMessageBox, 'warning', lambda *args: None) + + dialog.login_mode(True) + + assert dialog.is_admin == True + + def test_admin_login_wrong_password(self, qapp, temp_db, qtbot, monkeypatch): + dialog = LoginDialog(temp_db) + qtbot.addWidget(dialog) + + dialog.admin_input.setText("wrongpassword") + warning_called = [] + monkeypatch.setattr(QMessageBox, 'warning', lambda *args: warning_called.append(True)) + + dialog.login_mode(True) + + assert len(warning_called) == 1 + assert dialog.is_admin == False + + def test_user_login_correct_password(self, qapp, temp_db, qtbot, monkeypatch): + dialog = LoginDialog(temp_db) + qtbot.addWidget(dialog) + + dialog.user_input.setText("123456") + monkeypatch.setattr(QMessageBox, 'warning', lambda *args: None) + + dialog.login_mode(False) + + assert dialog.is_admin == False + + def test_user_login_wrong_password(self, qapp, temp_db, qtbot, monkeypatch): + dialog = LoginDialog(temp_db) + qtbot.addWidget(dialog) + + dialog.user_input.setText("wrongpassword") + warning_called = [] + monkeypatch.setattr(QMessageBox, 'warning', lambda *args: warning_called.append(True)) + + dialog.login_mode(False) + + assert len(warning_called) == 1 + + def test_get_stored_password(self, qapp, temp_db, qtbot): + dialog = LoginDialog(temp_db) + qtbot.addWidget(dialog) + + admin_pwd = dialog.get_stored_password("admin") + user_pwd = dialog.get_stored_password("user") + + assert admin_pwd == "123456" + assert user_pwd == "123456" + + def test_set_password(self, qapp, temp_db, qtbot): + dialog = LoginDialog(temp_db) + qtbot.addWidget(dialog) + + result = dialog.set_password("admin", "newpass123") + assert result == True + + new_pwd = dialog.get_stored_password("admin") + assert new_pwd == "newpass123" + + +@pytest.fixture +def fabric_manager_admin(qapp, temp_db, qtbot): + window = FabricManager.__new__(FabricManager) + window.is_admin = True + window.db_path = temp_db + from database import DatabaseManager + window.db_manager = DatabaseManager(temp_db) + from PyQt5.QtWidgets import QMainWindow + QMainWindow.__init__(window) + window.setWindowTitle("服装布料计算管理器 - 专业版 (管理员模式)") + window.resize(1300, 800) + window.init_ui() + window.load_garment_list() + qtbot.addWidget(window) + return window + + +@pytest.fixture +def fabric_manager_user(qapp, temp_db, qtbot): + window = FabricManager.__new__(FabricManager) + window.is_admin = False + window.db_path = temp_db + from database import DatabaseManager + window.db_manager = DatabaseManager(temp_db) + from PyQt5.QtWidgets import QMainWindow + QMainWindow.__init__(window) + window.setWindowTitle("服装布料计算管理器 - 专业版 (普通模式)") + window.resize(1300, 800) + window.init_ui() + window.load_garment_list() + qtbot.addWidget(window) + return window + + +class TestFabricManager: + """主窗口GUI测试""" + + def test_main_window_init_admin(self, fabric_manager_admin): + assert "管理员模式" in fabric_manager_admin.windowTitle() + + def test_main_window_init_user(self, fabric_manager_user): + assert "普通模式" in fabric_manager_user.windowTitle() + + def test_main_window_has_components(self, fabric_manager_admin): + assert hasattr(fabric_manager_admin, 'garment_combo') + assert hasattr(fabric_manager_admin, 'quantity_input') + assert hasattr(fabric_manager_admin, 'loss_input') + assert hasattr(fabric_manager_admin, 'result_text') + + def test_quantity_input_default(self, fabric_manager_admin): + assert fabric_manager_admin.quantity_input.value() == 1000 + + def test_loss_input_default(self, fabric_manager_admin): + assert fabric_manager_admin.loss_input.value() == 5.0 + + def test_unit_converter_components(self, fabric_manager_admin): + assert hasattr(fabric_manager_admin, 'calc_m') + assert hasattr(fabric_manager_admin, 'calc_yard') + assert hasattr(fabric_manager_admin, 'calc_kg') + assert hasattr(fabric_manager_admin, 'calc_width') + assert hasattr(fabric_manager_admin, 'calc_gsm') + + def test_unit_converter_defaults(self, fabric_manager_admin): + assert fabric_manager_admin.calc_width.value() == 150 + assert fabric_manager_admin.calc_gsm.value() == 200 + + def test_meter_to_yard_conversion(self, fabric_manager_admin): + fabric_manager_admin.calc_m.setValue(1.0) + expected_yard = 1.0 / 0.9144 + assert abs(fabric_manager_admin.calc_yard.value() - expected_yard) < 0.001 + + def test_quantity_input_change(self, fabric_manager_admin): + fabric_manager_admin.quantity_input.setValue(500) + assert fabric_manager_admin.quantity_input.value() == 500 + + def test_loss_input_change(self, fabric_manager_admin): + fabric_manager_admin.loss_input.setValue(10.0) + assert fabric_manager_admin.loss_input.value() == 10.0 diff --git a/test/test_login.py b/test/test_login.py new file mode 100644 index 0000000..8c10b66 --- /dev/null +++ b/test/test_login.py @@ -0,0 +1,127 @@ +""" +登录模块测试 - 测试密码验证和密码管理功能 +""" + +import unittest +import os +import tempfile +import gc +from database import DatabaseManager, get_db_connection + + +class TestLogin(unittest.TestCase): + """登录功能测试类""" + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test_login.db") + self.db_manager = DatabaseManager(self.db_path) + self.conn = None + + def tearDown(self): + if self.conn: + try: + self.conn.close() + except: + pass + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + if os.path.exists(self.temp_dir): + os.rmdir(self.temp_dir) + except: + pass + + def get_conn(self): + self.conn = get_db_connection(self.db_path) + return self.conn + + def test_default_admin_password(self): + """测试默认管理员密码""" + with self.get_conn() as conn: + cursor = conn.execute( + "SELECT value FROM admin_settings WHERE key = ?", + ("admin_password",) + ) + row = cursor.fetchone() + self.assertEqual(row[0], "123456") + + def test_default_user_password(self): + """测试默认普通用户密码""" + with self.get_conn() as conn: + cursor = conn.execute( + "SELECT value FROM admin_settings WHERE key = ?", + ("user_password",) + ) + row = cursor.fetchone() + self.assertEqual(row[0], "123456") + + def test_update_admin_password(self): + """测试更新管理员密码""" + with self.get_conn() as conn: + conn.execute( + "UPDATE admin_settings SET value = ? WHERE key = ?", + ("newpass123", "admin_password") + ) + conn.commit() + cursor = conn.execute( + "SELECT value FROM admin_settings WHERE key = ?", + ("admin_password",) + ) + row = cursor.fetchone() + self.assertEqual(row[0], "newpass123") + + def test_update_user_password(self): + """测试更新普通用户密码""" + with self.get_conn() as conn: + conn.execute( + "UPDATE admin_settings SET value = ? WHERE key = ?", + ("userpass456", "user_password") + ) + conn.commit() + cursor = conn.execute( + "SELECT value FROM admin_settings WHERE key = ?", + ("user_password",) + ) + row = cursor.fetchone() + self.assertEqual(row[0], "userpass456") + + def test_password_verification_correct(self): + """测试正确密码验证""" + with self.get_conn() as conn: + cursor = conn.execute( + "SELECT value FROM admin_settings WHERE key = ?", + ("admin_password",) + ) + stored_pwd = cursor.fetchone()[0] + self.assertEqual(stored_pwd, "123456") + + def test_password_verification_incorrect(self): + """测试错误密码验证""" + with self.get_conn() as conn: + cursor = conn.execute( + "SELECT value FROM admin_settings WHERE key = ?", + ("admin_password",) + ) + stored_pwd = cursor.fetchone()[0] + self.assertNotEqual(stored_pwd, "wrongpassword") + + def test_insert_or_replace_password(self): + """测试INSERT OR REPLACE密码设置""" + with self.get_conn() as conn: + conn.execute( + "INSERT OR REPLACE INTO admin_settings (key, value) VALUES (?, ?)", + ("admin_password", "replaced123") + ) + conn.commit() + cursor = conn.execute( + "SELECT value FROM admin_settings WHERE key = ?", + ("admin_password",) + ) + row = cursor.fetchone() + self.assertEqual(row[0], "replaced123") + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_login_gui.py b/test/test_login_gui.py new file mode 100644 index 0000000..4909215 --- /dev/null +++ b/test/test_login_gui.py @@ -0,0 +1,163 @@ +""" +登录对话框GUI测试模块 +使用PyQt测试框架测试登录和密码管理功能 +""" + +import unittest +import os +import sys +import tempfile +from PyQt5.QtWidgets import QApplication, QMessageBox, QInputDialog +from PyQt5.QtTest import QTest +from PyQt5.QtCore import Qt + +# 添加父目录到路径以导入模块 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from database import DatabaseManager +from login_dialog import LoginDialog + + +class TestLoginGUI(unittest.TestCase): + """登录对话框GUI测试类""" + + @classmethod + def setUpClass(cls): + """测试类初始化:创建QApplication实例""" + cls.app = QApplication.instance() + if cls.app is None: + cls.app = QApplication(sys.argv) + + def setUp(self): + """每个测试前准备:创建临时数据库和对话框""" + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test_fabric.db") + self.db_manager = DatabaseManager(self.db_path) + + # 创建对话框实例 + self.dialog = LoginDialog(self.db_path) + + # 保存原始消息框 + self._original_msgbox = QMessageBox.information + self._original_warning = QMessageBox.warning + self._original_critical = QMessageBox.critical + + # Mock消息框 + QMessageBox.information = lambda *args, **kwargs: None + QMessageBox.warning = lambda *args, **kwargs: None + QMessageBox.critical = lambda *args, **kwargs: None + + def tearDown(self): + """每个测试后清理""" + # 恢复消息框 + QMessageBox.information = self._original_msgbox + QMessageBox.warning = self._original_warning + QMessageBox.critical = self._original_critical + + # 关闭对话框 + self.dialog.close() + self.dialog.deleteLater() + + # 清理数据库 + import gc + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + if os.path.exists(self.temp_dir): + os.rmdir(self.temp_dir) + except: + pass + + # ========== 登录功能测试 ========== + + def test_admin_login_success(self): + """测试管理员登录成功""" + # 输入正确的管理员密码(默认123456) + self.dialog.admin_input.setText("123456") + + # 调用登录方法 + self.dialog.login_mode(True) + + # 验证登录成功 + self.assertTrue(self.dialog.is_admin, "应该设置为管理员模式") + + def test_admin_login_failure(self): + """测试管理员登录失败""" + warning_called = [] + def mock_warning(*args, **kwargs): + warning_called.append(args) + QMessageBox.warning = mock_warning + + # 输入错误的密码 + self.dialog.admin_input.setText("wrong_password") + + # 调用登录方法 + self.dialog.login_mode(True) + + # 验证登录失败 + self.assertFalse(self.dialog.is_admin, "不应该设置为管理员模式") + self.assertTrue(len(warning_called) > 0, "应该显示警告消息") + + def test_user_login_success(self): + """测试普通用户登录成功""" + # 输入正确的用户密码(默认123456) + self.dialog.user_input.setText("123456") + + # 调用登录方法 + self.dialog.login_mode(False) + + # 验证登录成功(is_admin应该为False) + self.assertFalse(self.dialog.is_admin, "应该是普通用户模式") + + def test_user_login_failure(self): + """测试普通用户登录失败""" + warning_called = [] + def mock_warning(*args, **kwargs): + warning_called.append(args) + QMessageBox.warning = mock_warning + + # 输入错误的密码 + self.dialog.user_input.setText("wrong_password") + + # 调用登录方法 + self.dialog.login_mode(False) + + # 验证登录失败 + self.assertTrue(len(warning_called) > 0, "应该显示警告消息") + + # ========== 密码管理测试 ========== + + def test_get_default_password(self): + """测试获取默认密码""" + admin_pwd = self.dialog.get_stored_password("admin") + user_pwd = self.dialog.get_stored_password("user") + + self.assertEqual(admin_pwd, "123456", "默认管理员密码应该是123456") + self.assertEqual(user_pwd, "123456", "默认用户密码应该是123456") + + def test_set_password(self): + """测试设置新密码""" + # 设置新的管理员密码 + result = self.dialog.set_password("admin", "new_admin_pass") + self.assertTrue(result, "设置密码应该成功") + + # 验证密码已更新 + stored_pwd = self.dialog.get_stored_password("admin") + self.assertEqual(stored_pwd, "new_admin_pass", "密码应该被更新") + + def test_login_with_new_password(self): + """测试使用新密码登录""" + # 设置新密码 + self.dialog.set_password("admin", "newpass123") + + # 使用新密码登录 + self.dialog.admin_input.setText("newpass123") + self.dialog.login_mode(True) + + # 验证登录成功 + self.assertTrue(self.dialog.is_admin, "应该使用新密码登录成功") + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_purchase_order.py b/test/test_purchase_order.py new file mode 100644 index 0000000..bd19150 --- /dev/null +++ b/test/test_purchase_order.py @@ -0,0 +1,111 @@ +""" +采购单生成模块测试 - 测试采购单生成和计算功能 +""" + +import unittest +import os +import tempfile +import gc +from database import DatabaseManager, get_db_connection + + +class TestPurchaseOrder(unittest.TestCase): + """采购单测试类""" + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test_po.db") + self.db_manager = DatabaseManager(self.db_path) + self.conn = None + self._setup_test_data() + + def tearDown(self): + if self.conn: + try: + self.conn.close() + except: + pass + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + if os.path.exists(self.temp_dir): + os.rmdir(self.temp_dir) + except: + pass + + def get_conn(self): + self.conn = get_db_connection(self.db_path) + return self.conn + + def _setup_test_data(self): + """准备测试数据""" + with get_db_connection(self.db_path) as conn: + conn.execute( + "INSERT INTO garments (style_number) VALUES (?)", + ("PO-001",) + ) + conn.execute(''' + INSERT INTO garment_materials + (style_number, category, model, usage_per_piece, unit) + VALUES (?, ?, ?, ?, ?) + ''', ("PO-001", "A料", "M001", 0.5, "米")) + conn.execute(''' + INSERT INTO garment_materials + (style_number, category, model, usage_per_piece, unit) + VALUES (?, ?, ?, ?, ?) + ''', ("PO-001", "B料", "M002", 0.3, "米")) + conn.commit() + + def test_get_materials_for_style(self): + """测试获取款式材料""" + with self.get_conn() as conn: + cursor = conn.execute(''' + SELECT category, model, usage_per_piece, unit + FROM garment_materials + WHERE style_number = ? AND usage_per_piece > 0 + ORDER BY id + ''', ("PO-001",)) + rows = cursor.fetchall() + self.assertEqual(len(rows), 2) + self.assertEqual(rows[0][0], "A料") + self.assertEqual(rows[1][0], "B料") + + def test_calculate_total_usage(self): + """测试计算总用量""" + usage_per_piece = 0.5 + quantity = 100 + loss_rate = 0.05 + total = usage_per_piece * quantity * (1 + loss_rate) + self.assertEqual(total, 52.5) + + def test_calculate_total_usage_no_loss(self): + """测试无损耗计算""" + usage_per_piece = 0.5 + quantity = 100 + loss_rate = 0.0 + total = usage_per_piece * quantity * (1 + loss_rate) + self.assertEqual(total, 50.0) + + def test_calculate_total_usage_high_loss(self): + """测试高损耗计算""" + usage_per_piece = 0.5 + quantity = 100 + loss_rate = 0.10 + total = usage_per_piece * quantity * (1 + loss_rate) + self.assertAlmostEqual(total, 55.0, places=2) + + def test_empty_style_materials(self): + """测试空款式材料""" + with self.get_conn() as conn: + cursor = conn.execute(''' + SELECT category, model, usage_per_piece, unit + FROM garment_materials + WHERE style_number = ? AND usage_per_piece > 0 + ''', ("NOT-EXIST",)) + rows = cursor.fetchall() + self.assertEqual(len(rows), 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_purchase_order_gui.py b/test/test_purchase_order_gui.py new file mode 100644 index 0000000..ec7bb92 --- /dev/null +++ b/test/test_purchase_order_gui.py @@ -0,0 +1,116 @@ +""" +采购单生成GUI测试模块 +使用PyQt测试框架测试采购单生成功能 +""" + +import unittest +import os +import sys +import tempfile +from PyQt5.QtWidgets import QApplication, QMessageBox +from PyQt5.QtTest import QTest +from PyQt5.QtCore import Qt + +# 添加父目录到路径以导入模块 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from database import DatabaseManager +from purchase_order_dialog import PurchaseOrderDialog + + +class TestPurchaseOrderGUI(unittest.TestCase): + """采购单生成GUI测试类""" + + @classmethod + def setUpClass(cls): + """测试类初始化:创建QApplication实例""" + cls.app = QApplication.instance() + if cls.app is None: + cls.app = QApplication(sys.argv) + + def setUp(self): + """每个测试前准备:创建临时数据库""" + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test_fabric.db") + self.db_manager = DatabaseManager(self.db_path) + + # 添加测试数据 + with self.db_manager.get_conn() as conn: + # 添加款式 + conn.execute(''' + INSERT INTO garments (style_number) + VALUES (?) + ''', ("TEST-STYLE-001",)) + + # 添加原料 + conn.execute(''' + INSERT INTO fabrics (model, category, color, unit) + VALUES (?, ?, ?, ?) + ''', ("TEST-FABRIC-001", "布料", "白色", "米")) + + # 添加款式材料用量 + conn.execute(''' + INSERT INTO garment_materials (style_number, category, model, usage_per_piece, unit) + VALUES (?, ?, ?, ?, ?) + ''', ("TEST-STYLE-001", "布料", "TEST-FABRIC-001", 2.5, "米")) + conn.commit() + + # 保存原始消息框 + self._original_msgbox = QMessageBox.information + self._original_warning = QMessageBox.warning + + # Mock消息框 + QMessageBox.information = lambda *args, **kwargs: None + QMessageBox.warning = lambda *args, **kwargs: None + + def tearDown(self): + """每个测试后清理""" + # 恢复消息框 + QMessageBox.information = self._original_msgbox + QMessageBox.warning = self._original_warning + + # 清理数据库 + import gc + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + if os.path.exists(self.temp_dir): + os.rmdir(self.temp_dir) + except: + pass + + # ========== 采购单生成测试 ========== + + def test_generate_purchase_order(self): + """测试生成采购单""" + # 创建采购单对话框 + dialog = PurchaseOrderDialog(self.db_path, "TEST-STYLE-001", 100, 0.05) + + # 验证采购单文本已生成 + po_text = dialog.po_text.toPlainText() + self.assertIn("TEST-STYLE-001", po_text, "采购单应包含款号") + self.assertIn("100 件", po_text, "采购单应包含生产数量") + self.assertIn("5.0%", po_text, "采购单应包含损耗率") + + dialog.close() + dialog.deleteLater() + + def test_material_calculation(self): + """测试材料用量计算""" + # 创建采购单对话框(100件,损耗率5%) + dialog = PurchaseOrderDialog(self.db_path, "TEST-STYLE-001", 100, 0.05) + + # 验证采购单包含材料信息 + po_text = dialog.po_text.toPlainText() + self.assertIn("TEST-FABRIC-001", po_text, "采购单应包含原料型号") + + # 计算预期用量:100件 * 2.5米/件 * (1 + 0.05) = 262.5米 + self.assertIn("262.5", po_text, "采购单应包含正确的用量计算") + + dialog.close() + dialog.deleteLater() + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_raw_material.py b/test/test_raw_material.py new file mode 100644 index 0000000..060995f --- /dev/null +++ b/test/test_raw_material.py @@ -0,0 +1,265 @@ +""" +原料管理功能测试模块 +测试添加、删除、编辑原料功能 +""" + +import unittest +import os +import tempfile +from database import DatabaseManager, get_db_connection + + +class TestRawMaterial(unittest.TestCase): + """原料管理测试类""" + + def setUp(self): + """测试前准备:创建临时数据库""" + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test_fabric.db") + self.db_manager = DatabaseManager(self.db_path) + self.conn = None + + def tearDown(self): + """测试后清理:删除临时数据库""" + if self.conn: + try: + self.conn.close() + except: + pass + import gc + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + if os.path.exists(self.temp_dir): + os.rmdir(self.temp_dir) + except: + pass + + def get_conn(self): + """获取数据库连接""" + self.conn = get_db_connection(self.db_path) + return self.conn + + # ========== 添加原料测试 ========== + + def test_add_raw_material_basic(self): + """测试基本添加原料功能""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, fabric_type, supplier, color, width, gsm, unit, retail_price, bulk_price, timestamp) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) + ''', ("TEST-001", "布料", "棉布", "供应商A", "白色", 150.0, 200.0, "米", 10.0, 8.0)) + conn.commit() + + cursor = conn.execute("SELECT * FROM fabrics WHERE model = ?", ("TEST-001",)) + row = cursor.fetchone() + + self.assertIsNotNone(row) + self.assertEqual(row[0], "TEST-001") + + def test_add_raw_material_with_all_fields(self): + """测试添加包含所有字段的原料""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, fabric_type, supplier, color, width, gsm, unit, retail_price, bulk_price, timestamp) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) + ''', ("TEST-002", "辅料", "拉链", "供应商B", "黑色", 0, 0, "条", 5.0, 4.0)) + conn.commit() + + cursor = conn.execute("SELECT category, fabric_type, unit FROM fabrics WHERE model = ?", ("TEST-002",)) + row = cursor.fetchone() + + self.assertEqual(row[0], "辅料") + self.assertEqual(row[1], "拉链") + self.assertEqual(row[2], "条") + + def test_add_raw_material_duplicate_model(self): + """测试添加重复型号原料(应拒绝,模拟GUI逻辑)""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, supplier, timestamp) + VALUES (?, ?, ?, datetime('now')) + ''', ("TEST-003", "布料", "供应商A")) + conn.commit() + + model = "TEST-003" + current_edit_model = None + + should_reject = False + if not current_edit_model: + cursor = conn.execute("SELECT 1 FROM fabrics WHERE model = ?", (model,)) + if cursor.fetchone(): + should_reject = True + + self.assertTrue(should_reject, "应拒绝添加重复型号") + + cursor = conn.execute("SELECT category, supplier FROM fabrics WHERE model = ?", (model,)) + row = cursor.fetchone() + self.assertEqual(row[0], "布料", "原数据不应被覆盖") + self.assertEqual(row[1], "供应商A", "原数据不应被覆盖") + + def test_add_raw_material_empty_model(self): + """测试添加空型号原料(应失败)""" + with self.get_conn() as conn: + try: + conn.execute(''' + INSERT INTO fabrics (model, category, timestamp) + VALUES (?, ?, datetime('now')) + ''', ("", "布料")) + conn.commit() + cursor = conn.execute("SELECT COUNT(*) FROM fabrics WHERE model = ''") + count = cursor.fetchone()[0] + self.assertEqual(count, 1) + except Exception: + pass + + # ========== 删除原料测试 ========== + + def test_delete_raw_material_basic(self): + """测试基本删除原料功能""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, timestamp) + VALUES (?, ?, datetime('now')) + ''', ("DEL-001", "布料")) + conn.commit() + + conn.execute("DELETE FROM fabrics WHERE model = ?", ("DEL-001",)) + conn.commit() + + cursor = conn.execute("SELECT * FROM fabrics WHERE model = ?", ("DEL-001",)) + row = cursor.fetchone() + + self.assertIsNone(row) + + def test_delete_raw_material_not_exist(self): + """测试删除不存在的原料""" + with self.get_conn() as conn: + cursor = conn.execute("DELETE FROM fabrics WHERE model = ?", ("NOT-EXIST",)) + conn.commit() + self.assertEqual(cursor.rowcount, 0) + + def test_delete_raw_material_with_stock(self): + """测试删除有库存记录的原料""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, timestamp) + VALUES (?, ?, datetime('now')) + ''', ("DEL-002", "布料")) + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date) + VALUES (?, ?, ?, ?) + ''', ("DEL-002", 100.0, "米", "2024-01-01")) + conn.commit() + + conn.execute("DELETE FROM fabrics WHERE model = ?", ("DEL-002",)) + conn.commit() + + cursor = conn.execute("SELECT * FROM fabrics WHERE model = ?", ("DEL-002",)) + fabric_row = cursor.fetchone() + + cursor = conn.execute("SELECT * FROM fabric_stock_in WHERE model = ?", ("DEL-002",)) + stock_row = cursor.fetchone() + + self.assertIsNone(fabric_row) + self.assertIsNotNone(stock_row) + + # ========== 编辑原料测试 ========== + + def test_edit_raw_material_basic(self): + """测试基本编辑原料功能""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, supplier, timestamp) + VALUES (?, ?, ?, datetime('now')) + ''', ("EDIT-001", "布料", "供应商A")) + conn.commit() + + conn.execute(''' + UPDATE fabrics SET supplier = ?, updated_at = CURRENT_TIMESTAMP + WHERE model = ? + ''', ("供应商B", "EDIT-001")) + conn.commit() + + cursor = conn.execute("SELECT supplier FROM fabrics WHERE model = ?", ("EDIT-001",)) + row = cursor.fetchone() + + self.assertEqual(row[0], "供应商B") + + def test_edit_raw_material_category(self): + """测试编辑原料类目""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, fabric_type, timestamp) + VALUES (?, ?, ?, datetime('now')) + ''', ("EDIT-002", "布料", "棉布")) + conn.commit() + + conn.execute(''' + UPDATE fabrics SET category = ?, fabric_type = ? + WHERE model = ? + ''', ("辅料", "纽扣", "EDIT-002")) + conn.commit() + + cursor = conn.execute("SELECT category, fabric_type FROM fabrics WHERE model = ?", ("EDIT-002",)) + row = cursor.fetchone() + + self.assertEqual(row[0], "辅料") + self.assertEqual(row[1], "纽扣") + + def test_edit_raw_material_price(self): + """测试编辑原料价格""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, retail_price, bulk_price, timestamp) + VALUES (?, ?, ?, datetime('now')) + ''', ("EDIT-003", 10.0, 8.0)) + conn.commit() + + conn.execute(''' + UPDATE fabrics SET retail_price = ?, bulk_price = ? + WHERE model = ? + ''', (15.0, 12.0, "EDIT-003")) + conn.commit() + + cursor = conn.execute("SELECT retail_price, bulk_price FROM fabrics WHERE model = ?", ("EDIT-003",)) + row = cursor.fetchone() + + self.assertEqual(row[0], 15.0) + self.assertEqual(row[1], 12.0) + + def test_edit_raw_material_specifications(self): + """测试编辑原料规格(幅宽、克重)""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, width, gsm, timestamp) + VALUES (?, ?, ?, datetime('now')) + ''', ("EDIT-004", 150.0, 200.0)) + conn.commit() + + conn.execute(''' + UPDATE fabrics SET width = ?, gsm = ? + WHERE model = ? + ''', (160.0, 250.0, "EDIT-004")) + conn.commit() + + cursor = conn.execute("SELECT width, gsm FROM fabrics WHERE model = ?", ("EDIT-004",)) + row = cursor.fetchone() + + self.assertEqual(row[0], 160.0) + self.assertEqual(row[1], 250.0) + + def test_edit_raw_material_not_exist(self): + """测试编辑不存在的原料""" + with self.get_conn() as conn: + cursor = conn.execute(''' + UPDATE fabrics SET supplier = ? + WHERE model = ? + ''', ("供应商X", "NOT-EXIST")) + conn.commit() + self.assertEqual(cursor.rowcount, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_raw_material_gui.py b/test/test_raw_material_gui.py new file mode 100644 index 0000000..5d3936f --- /dev/null +++ b/test/test_raw_material_gui.py @@ -0,0 +1,278 @@ +""" +原料管理GUI测试模块 +使用PyQt测试框架测试GUI交互和业务逻辑 +""" + +import unittest +import os +import sys +import tempfile +from PyQt5.QtWidgets import QApplication, QMessageBox, QTabWidget +from PyQt5.QtTest import QTest +from PyQt5.QtCore import Qt + +# 添加父目录到路径以导入模块 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from database import DatabaseManager +from raw_material_dialog import RawMaterialLibraryDialog + + +class TestRawMaterialGUI(unittest.TestCase): + """原料管理GUI测试类""" + + @classmethod + def setUpClass(cls): + """测试类初始化:创建QApplication实例""" + cls.app = QApplication.instance() + if cls.app is None: + cls.app = QApplication(sys.argv) + + def setUp(self): + """每个测试前准备:创建临时数据库和对话框""" + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test_fabric.db") + self.db_manager = DatabaseManager(self.db_path) + + # 创建对话框实例(管理员模式) + self.dialog = RawMaterialLibraryDialog(self.db_path, is_admin=True) + + # 禁用消息框以便自动化测试 + self._original_msgbox = QMessageBox.information + self._original_warning = QMessageBox.warning + self._original_critical = QMessageBox.critical + self._original_question = QMessageBox.question + + # Mock消息框 + QMessageBox.information = lambda *args, **kwargs: None + QMessageBox.warning = lambda *args, **kwargs: None + QMessageBox.critical = lambda *args, **kwargs: None + QMessageBox.question = lambda *args, **kwargs: QMessageBox.Yes + + def tearDown(self): + """每个测试后清理""" + # 恢复消息框 + QMessageBox.information = self._original_msgbox + QMessageBox.warning = self._original_warning + QMessageBox.critical = self._original_critical + QMessageBox.question = self._original_question + + # 关闭对话框 + self.dialog.close() + self.dialog.deleteLater() + + # 清理数据库 + import gc + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + if os.path.exists(self.temp_dir): + os.rmdir(self.temp_dir) + except: + pass + + # ========== 添加原料GUI测试 ========== + + def test_add_raw_material_basic_gui(self): + """测试通过GUI添加基本原料""" + # 切换到新增/编辑标签页 + tabs = self.dialog.findChild(QTabWidget) + tabs.setCurrentIndex(1) + + # 填写表单 + self.dialog.add_major_category.setCurrentText("布料") + self.dialog.add_sub_category.setText("棉布") + self.dialog.add_model.setText("GUI-TEST-001") + self.dialog.add_supplier.setCurrentText("测试供应商") + self.dialog.add_color.setText("白色") + self.dialog.add_width.setValue(150.0) + self.dialog.add_gsm.setValue(200.0) + self.dialog.add_unit.setCurrentText("米") + self.dialog.add_retail.setValue(10.0) + self.dialog.add_bulk.setValue(8.0) + + # 点击保存按钮 + self.dialog.save_raw_material() + + # 验证数据已保存到数据库 + with self.dialog.get_conn() as conn: + cursor = conn.execute("SELECT * FROM fabrics WHERE model = ?", ("GUI-TEST-001",)) + row = cursor.fetchone() + + self.assertIsNotNone(row, "原料应该被保存到数据库") + self.assertEqual(row[0], "GUI-TEST-001", "型号应该匹配") + + def test_add_raw_material_duplicate_model_gui(self): + """测试通过GUI添加重复型号(应被拒绝)""" + # 先添加一个原料 + with self.dialog.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, supplier, timestamp) + VALUES (?, ?, ?, datetime('now')) + ''', ("GUI-TEST-002", "布料", "供应商A")) + conn.commit() + + # Mock warning消息框以捕获警告 + warning_called = [] + def mock_warning(*args, **kwargs): + warning_called.append(args) + QMessageBox.warning = mock_warning + + # 尝试添加重复型号 + tabs = self.dialog.findChild(QTabWidget) + tabs.setCurrentIndex(1) + + self.dialog.add_model.setText("GUI-TEST-002") + self.dialog.add_major_category.setCurrentText("布料") + self.dialog.save_raw_material() + + # 验证警告被触发 + self.assertTrue(len(warning_called) > 0, "应该显示警告消息") + + # 验证数据库中只有一条记录 + with self.dialog.get_conn() as conn: + cursor = conn.execute("SELECT COUNT(*) FROM fabrics WHERE model = ?", ("GUI-TEST-002",)) + count = cursor.fetchone()[0] + + self.assertEqual(count, 1, "数据库中应该只有一条记录") + + def test_add_raw_material_empty_model_gui(self): + """测试通过GUI添加空型号(应被拒绝)""" + warning_called = [] + def mock_warning(*args, **kwargs): + warning_called.append(args) + QMessageBox.warning = mock_warning + + # 尝试保存空型号 + tabs = self.dialog.findChild(QTabWidget) + tabs.setCurrentIndex(1) + + self.dialog.add_model.setText("") + self.dialog.save_raw_material() + + # 验证警告被触发 + self.assertTrue(len(warning_called) > 0, "应该显示警告消息") + + # ========== 编辑原料GUI测试 ========== + + def test_edit_raw_material_gui(self): + """测试通过GUI编辑原料""" + # 先添加一个原料 + with self.dialog.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, supplier, color, timestamp) + VALUES (?, ?, ?, ?, datetime('now')) + ''', ("GUI-EDIT-001", "布料", "供应商A", "白色")) + conn.commit() + + # 刷新表格 + self.dialog.load_table() + + # 调用编辑方法 + self.dialog.edit_raw_material("GUI-EDIT-001") + + # 验证表单已填充 + self.assertEqual(self.dialog.add_model.text(), "GUI-EDIT-001") + self.assertEqual(self.dialog.add_supplier.currentText(), "供应商A") + self.assertEqual(self.dialog.add_color.text(), "白色") + + # 修改供应商 + self.dialog.add_supplier.setCurrentText("供应商B") + self.dialog.save_raw_material() + + # 验证修改已保存 + with self.dialog.get_conn() as conn: + cursor = conn.execute("SELECT supplier FROM fabrics WHERE model = ?", ("GUI-EDIT-001",)) + row = cursor.fetchone() + + self.assertEqual(row[0], "供应商B", "供应商应该被更新") + + # ========== 删除原料GUI测试 ========== + + def test_delete_raw_material_gui(self): + """测试通过GUI删除原料""" + # 先添加一个原料 + with self.dialog.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, timestamp) + VALUES (?, ?, datetime('now')) + ''', ("GUI-DEL-001", "布料")) + conn.commit() + + # 刷新表格 + self.dialog.load_table() + + # 调用删除方法 + self.dialog.delete_raw("GUI-DEL-001") + + # 验证已删除 + with self.dialog.get_conn() as conn: + cursor = conn.execute("SELECT * FROM fabrics WHERE model = ?", ("GUI-DEL-001",)) + row = cursor.fetchone() + + self.assertIsNone(row, "原料应该被删除") + + # ========== 过滤和搜索GUI测试 ========== + + def test_filter_by_category_gui(self): + """测试通过GUI按类目过滤""" + # 添加测试数据 + with self.dialog.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, fabric_type, timestamp) + VALUES (?, ?, ?, datetime('now')) + ''', ("FILTER-001", "布料", "棉布")) + conn.execute(''' + INSERT INTO fabrics (model, category, fabric_type, timestamp) + VALUES (?, ?, ?, datetime('now')) + ''', ("FILTER-002", "辅料", "拉链")) + conn.commit() + + # 刷新过滤器和表格 + self.dialog.refresh_filters_and_table() + + # 选择"布料"类目 + self.dialog.major_combo.setCurrentText("布料") + + # 验证表格只显示布料 + row_count = self.dialog.table.rowCount() + for row in range(row_count): + category_item = self.dialog.table.item(row, 0) + if category_item: + self.assertEqual(category_item.text(), "布料", "应该只显示布料类目") + + def test_search_by_model_gui(self): + """测试通过GUI搜索型号""" + # 添加测试数据 + with self.dialog.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, timestamp) + VALUES (?, ?, datetime('now')) + ''', ("SEARCH-001", "布料")) + conn.execute(''' + INSERT INTO fabrics (model, category, timestamp) + VALUES (?, ?, datetime('now')) + ''', ("SEARCH-002", "布料")) + conn.execute(''' + INSERT INTO fabrics (model, category, timestamp) + VALUES (?, ?, datetime('now')) + ''', ("OTHER-001", "布料")) + conn.commit() + + # 刷新表格 + self.dialog.load_table() + + # 输入搜索关键词 + self.dialog.search_input.setText("SEARCH") + + # 验证表格只显示匹配的结果 + row_count = self.dialog.table.rowCount() + for row in range(row_count): + model_item = self.dialog.table.item(row, 2) + if model_item: + self.assertIn("SEARCH", model_item.text(), "应该只显示包含SEARCH的型号") + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_stock.py b/test/test_stock.py new file mode 100644 index 0000000..7b6b672 --- /dev/null +++ b/test/test_stock.py @@ -0,0 +1,272 @@ +""" +库存管理模块测试 - 测试入库和库存查询功能 +""" + +import unittest +import os +import tempfile +import gc +from datetime import datetime +from database import DatabaseManager, get_db_connection + + +class TestStock(unittest.TestCase): + """库存管理测试类""" + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test_stock.db") + self.db_manager = DatabaseManager(self.db_path) + self.conn = None + self._setup_test_data() + + def tearDown(self): + if self.conn: + try: + self.conn.close() + except: + pass + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + if os.path.exists(self.temp_dir): + os.rmdir(self.temp_dir) + except: + pass + + def get_conn(self): + self.conn = get_db_connection(self.db_path) + return self.conn + + def _setup_test_data(self): + """准备测试数据""" + with get_db_connection(self.db_path) as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, supplier, color, unit, timestamp) + VALUES (?, ?, ?, ?, ?, datetime('now')) + ''', ("STOCK-001", "布料", "供应商A", "白色", "米")) + conn.commit() + + # ========== 入库测试 ========== + + def test_stock_in_basic(self): + """测试基本入库功能""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date, note) + VALUES (?, ?, ?, ?, ?) + ''', ("STOCK-001", 100.0, "米", "2024-01-01", "测试入库")) + conn.commit() + cursor = conn.execute( + "SELECT quantity, note FROM fabric_stock_in WHERE model = ?", + ("STOCK-001",) + ) + row = cursor.fetchone() + self.assertEqual(row[0], 100.0) + self.assertEqual(row[1], "测试入库") + + def test_stock_in_multiple(self): + """测试多次入库""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date) + VALUES (?, ?, ?, ?) + ''', ("STOCK-001", 50.0, "米", "2024-01-01")) + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date) + VALUES (?, ?, ?, ?) + ''', ("STOCK-001", 30.0, "米", "2024-01-02")) + conn.commit() + cursor = conn.execute( + "SELECT SUM(quantity) FROM fabric_stock_in WHERE model = ?", + ("STOCK-001",) + ) + total = cursor.fetchone()[0] + self.assertEqual(total, 80.0) + + def test_stock_in_with_note(self): + """测试带备注的入库""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date, note) + VALUES (?, ?, ?, ?, ?) + ''', ("STOCK-001", 25.5, "米", "2024-01-15", "批次号:B001")) + conn.commit() + cursor = conn.execute( + "SELECT note FROM fabric_stock_in WHERE model = ? AND quantity = ?", + ("STOCK-001", 25.5) + ) + row = cursor.fetchone() + self.assertEqual(row[0], "批次号:B001") + + # ========== 消耗测试 ========== + + def test_consumption_basic(self): + """测试基本消耗记录""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabric_consumption + (style_number, model, single_usage, quantity_made, loss_rate, consume_quantity, consume_date, unit) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', ("G001", "STOCK-001", 0.5, 100, 0.05, 52.5, "2024-01-10", "米")) + conn.commit() + cursor = conn.execute( + "SELECT consume_quantity FROM fabric_consumption WHERE model = ?", + ("STOCK-001",) + ) + row = cursor.fetchone() + self.assertEqual(row[0], 52.5) + + def test_consumption_calculation(self): + """测试消耗量计算""" + single_usage = 0.5 + quantity_made = 100 + loss_rate = 0.05 + expected = single_usage * quantity_made * (1 + loss_rate) + self.assertEqual(expected, 52.5) + + # ========== 库存计算测试 ========== + + def test_stock_remaining_calculation(self): + """测试剩余库存计算""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date) + VALUES (?, ?, ?, ?) + ''', ("STOCK-001", 100.0, "米", "2024-01-01")) + conn.execute(''' + INSERT INTO fabric_consumption + (style_number, model, consume_quantity, consume_date, unit) + VALUES (?, ?, ?, ?, ?) + ''', ("G001", "STOCK-001", 30.0, "2024-01-05", "米")) + conn.commit() + + cursor_in = conn.execute( + "SELECT COALESCE(SUM(quantity), 0) FROM fabric_stock_in WHERE model = ?", + ("STOCK-001",) + ) + total_in = cursor_in.fetchone()[0] + + cursor_out = conn.execute( + "SELECT COALESCE(SUM(consume_quantity), 0) FROM fabric_consumption WHERE model = ?", + ("STOCK-001",) + ) + total_out = cursor_out.fetchone()[0] + + remaining = total_in - total_out + self.assertEqual(remaining, 70.0) + + def test_stock_zero_remaining(self): + """测试库存清零""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date) + VALUES (?, ?, ?, ?) + ''', ("STOCK-001", 50.0, "米", "2024-01-01")) + conn.execute(''' + INSERT INTO fabric_consumption + (style_number, model, consume_quantity, consume_date, unit) + VALUES (?, ?, ?, ?, ?) + ''', ("库存清零", "STOCK-001", 50.0, "2024-01-10", "米")) + conn.commit() + + cursor_in = conn.execute( + "SELECT COALESCE(SUM(quantity), 0) FROM fabric_stock_in WHERE model = ?", + ("STOCK-001",) + ) + total_in = cursor_in.fetchone()[0] + + cursor_out = conn.execute( + "SELECT COALESCE(SUM(consume_quantity), 0) FROM fabric_consumption WHERE model = ?", + ("STOCK-001",) + ) + total_out = cursor_out.fetchone()[0] + + remaining = total_in - total_out + self.assertEqual(remaining, 0.0) + + # ========== 编辑库存并清除历史记录测试 ========== + + def test_edit_stock_and_clear_history(self): + """测试编辑剩余库存并清除历史记录""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date) + VALUES (?, ?, ?, ?) + ''', ("STOCK-001", 100.0, "米", "2024-01-01")) + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date) + VALUES (?, ?, ?, ?) + ''', ("STOCK-001", 50.0, "米", "2024-01-05")) + conn.execute(''' + INSERT INTO fabric_consumption + (style_number, model, consume_quantity, consume_date, unit) + VALUES (?, ?, ?, ?, ?) + ''', ("G001", "STOCK-001", 30.0, "2024-01-10", "米")) + conn.commit() + + conn.execute("DELETE FROM fabric_stock_in WHERE model = ?", ("STOCK-001",)) + conn.execute("DELETE FROM fabric_consumption WHERE model = ?", ("STOCK-001",)) + new_stock = 80.0 + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date, note) + VALUES (?, ?, ?, ?, ?) + ''', ("STOCK-001", new_stock, "米", datetime.now().strftime('%Y-%m-%d'), "库存盘点调整")) + conn.commit() + + cursor_in = conn.execute( + "SELECT COUNT(*) FROM fabric_stock_in WHERE model = ?", ("STOCK-001",) + ) + in_count = cursor_in.fetchone()[0] + + cursor_out = conn.execute( + "SELECT COUNT(*) FROM fabric_consumption WHERE model = ?", ("STOCK-001",) + ) + out_count = cursor_out.fetchone()[0] + + cursor_qty = conn.execute( + "SELECT SUM(quantity) FROM fabric_stock_in WHERE model = ?", ("STOCK-001",) + ) + total_qty = cursor_qty.fetchone()[0] + + self.assertEqual(in_count, 1) + self.assertEqual(out_count, 0) + self.assertEqual(total_qty, 80.0) + + def test_edit_stock_clear_history_multiple_models(self): + """测试编辑库存时只清除指定型号的历史记录""" + with self.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, unit) + VALUES (?, ?, ?) + ''', ("STOCK-002", "布料", "米")) + + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date) + VALUES (?, ?, ?, ?) + ''', ("STOCK-001", 100.0, "米", "2024-01-01")) + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date) + VALUES (?, ?, ?, ?) + ''', ("STOCK-002", 200.0, "米", "2024-01-01")) + conn.commit() + + conn.execute("DELETE FROM fabric_stock_in WHERE model = ?", ("STOCK-001",)) + conn.execute("DELETE FROM fabric_consumption WHERE model = ?", ("STOCK-001",)) + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date, note) + VALUES (?, ?, ?, ?, ?) + ''', ("STOCK-001", 50.0, "米", datetime.now().strftime('%Y-%m-%d'), "库存盘点调整")) + conn.commit() + + cursor_002 = conn.execute( + "SELECT SUM(quantity) FROM fabric_stock_in WHERE model = ?", ("STOCK-002",) + ) + stock_002 = cursor_002.fetchone()[0] + + self.assertEqual(stock_002, 200.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_stock_gui.py b/test/test_stock_gui.py new file mode 100644 index 0000000..b1e94a4 --- /dev/null +++ b/test/test_stock_gui.py @@ -0,0 +1,161 @@ +""" +库存管理GUI测试模块 +使用PyQt测试框架测试库存入库和查询功能 +""" + +import unittest +import os +import sys +import tempfile +from PyQt5.QtWidgets import QApplication, QMessageBox, QInputDialog +from PyQt5.QtTest import QTest +from PyQt5.QtCore import Qt + +# 添加父目录到路径以导入模块 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from database import DatabaseManager +from stock_dialog import StockInDialog + + +class TestStockGUI(unittest.TestCase): + """库存管理GUI测试类""" + + @classmethod + def setUpClass(cls): + """测试类初始化:创建QApplication实例""" + cls.app = QApplication.instance() + if cls.app is None: + cls.app = QApplication(sys.argv) + + def setUp(self): + """每个测试前准备:创建临时数据库和对话框""" + self.temp_dir = tempfile.mkdtemp() + self.db_path = os.path.join(self.temp_dir, "test_fabric.db") + self.db_manager = DatabaseManager(self.db_path) + + # 添加测试原料 + with self.db_manager.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, color, supplier, unit, timestamp) + VALUES (?, ?, ?, ?, ?, datetime('now')) + ''', ("TEST-STOCK-001", "布料", "白色", "供应商A", "米")) + conn.commit() + + # 创建对话框实例 + self.dialog = StockInDialog(self.db_path) + + # 保存原始消息框和输入框 + self._original_msgbox = QMessageBox.information + self._original_warning = QMessageBox.warning + self._original_input = QInputDialog.getDouble + + # Mock消息框 + QMessageBox.information = lambda *args, **kwargs: None + QMessageBox.warning = lambda *args, **kwargs: None + + def tearDown(self): + """每个测试后清理""" + # 恢复消息框 + QMessageBox.information = self._original_msgbox + QMessageBox.warning = self._original_warning + QInputDialog.getDouble = self._original_input + + # 关闭对话框 + self.dialog.close() + self.dialog.deleteLater() + + # 清理数据库 + import gc + gc.collect() + try: + if os.path.exists(self.db_path): + os.remove(self.db_path) + if os.path.exists(self.temp_dir): + os.rmdir(self.temp_dir) + except: + pass + + # ========== 库存加载测试 ========== + + def test_load_models(self): + """测试加载原料列表""" + self.dialog.load_models() + + # 验证表格有数据 + self.assertGreater(self.dialog.table.rowCount(), 0, "表格应该有数据") + + # 验证第一行是测试数据 + model_item = self.dialog.table.item(0, 0) + self.assertIsNotNone(model_item, "应该有型号数据") + self.assertEqual(model_item.text(), "TEST-STOCK-001", "型号应该匹配") + + def test_search_models(self): + """测试搜索原料""" + # 添加更多测试数据 + with self.dialog.get_conn() as conn: + conn.execute(''' + INSERT INTO fabrics (model, category, color, unit, timestamp) + VALUES (?, ?, ?, ?, datetime('now')) + ''', ("OTHER-001", "布料", "黑色", "米")) + conn.commit() + + # 搜索特定型号 + self.dialog.search_input.setText("TEST-STOCK") + self.dialog.load_models() + + # 验证只显示匹配的结果 + row_count = self.dialog.table.rowCount() + for row in range(row_count): + model_item = self.dialog.table.item(row, 0) + if model_item: + self.assertIn("TEST-STOCK", model_item.text(), "应该只显示匹配的型号") + + # ========== 库存计算测试 ========== + + def test_stock_calculation(self): + """测试库存数量计算""" + # 添加入库记录 + with self.dialog.get_conn() as conn: + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date) + VALUES (?, ?, ?, ?) + ''', ("TEST-STOCK-001", 100.0, "米", "2024-01-01")) + conn.commit() + + # 刷新表格 + self.dialog.load_models() + + # 验证库存显示 + remaining_item = self.dialog.table.item(0, 4) + self.assertIsNotNone(remaining_item, "应该有库存数据") + remaining = float(remaining_item.text()) + self.assertEqual(remaining, 100.0, "库存应该是100.0") + + def test_stock_with_consumption(self): + """测试库存扣除消耗后的计算""" + # 添加入库记录 + with self.dialog.get_conn() as conn: + conn.execute(''' + INSERT INTO fabric_stock_in (model, quantity, unit, purchase_date) + VALUES (?, ?, ?, ?) + ''', ("TEST-STOCK-001", 100.0, "米", "2024-01-01")) + + # 添加消耗记录 + conn.execute(''' + INSERT INTO fabric_consumption (model, style_number, consume_quantity, consume_date, unit) + VALUES (?, ?, ?, ?, ?) + ''', ("TEST-STOCK-001", "款式001", 30.0, "2024-01-02", "米")) + conn.commit() + + # 刷新表格 + self.dialog.load_models() + + # 验证库存显示(100 - 30 = 70) + remaining_item = self.dialog.table.item(0, 4) + remaining = float(remaining_item.text()) + self.assertEqual(remaining, 70.0, "库存应该是70.0(100-30)") + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_unit_conversion.py b/test/test_unit_conversion.py new file mode 100644 index 0000000..9bd558b --- /dev/null +++ b/test/test_unit_conversion.py @@ -0,0 +1,142 @@ +""" +单位转换测试 +""" + +import unittest +import math + + +YARD_TO_METER = 0.9144 + + +def convert_meter_to_yard(meters): + return meters / YARD_TO_METER + + +def convert_yard_to_meter(yards): + return yards * YARD_TO_METER + + +def convert_meter_to_kg(meters, width_cm, gsm): + return meters * (width_cm / 100) * (gsm / 1000) + + +def convert_yard_to_kg(yards, width_cm, gsm): + meters = yards * YARD_TO_METER + return meters * (width_cm / 100) * (gsm / 1000) + + +def convert_kg_to_meter(kg, width_cm, gsm): + return kg / ((width_cm / 100) * (gsm / 1000)) + + +def convert_kg_to_yard(kg, width_cm, gsm): + meters = kg / ((width_cm / 100) * (gsm / 1000)) + return meters / YARD_TO_METER + + +class TestMeterYardConversion(unittest.TestCase): + + def test_meter_to_yard(self): + result = convert_meter_to_yard(1) + self.assertAlmostEqual(result, 1.0936, places=4) + + def test_yard_to_meter(self): + result = convert_yard_to_meter(1) + self.assertAlmostEqual(result, 0.9144, places=4) + + def test_meter_yard_roundtrip(self): + original = 10.5 + yards = convert_meter_to_yard(original) + back = convert_yard_to_meter(yards) + self.assertAlmostEqual(original, back, places=6) + + def test_yard_meter_roundtrip(self): + original = 15.3 + meters = convert_yard_to_meter(original) + back = convert_meter_to_yard(meters) + self.assertAlmostEqual(original, back, places=6) + + +class TestLengthToWeightConversion(unittest.TestCase): + + def setUp(self): + self.width_cm = 150 + self.gsm = 200 + + def test_meter_to_kg(self): + result = convert_meter_to_kg(10, self.width_cm, self.gsm) + expected = 10 * 1.5 * 0.2 + self.assertAlmostEqual(result, expected, places=6) + + def test_yard_to_kg(self): + result = convert_yard_to_kg(10, self.width_cm, self.gsm) + meters = 10 * YARD_TO_METER + expected = meters * 1.5 * 0.2 + self.assertAlmostEqual(result, expected, places=6) + + def test_kg_to_meter(self): + result = convert_kg_to_meter(3, self.width_cm, self.gsm) + expected = 3 / (1.5 * 0.2) + self.assertAlmostEqual(result, expected, places=6) + + def test_kg_to_yard(self): + result = convert_kg_to_yard(3, self.width_cm, self.gsm) + meters = 3 / (1.5 * 0.2) + expected = meters / YARD_TO_METER + self.assertAlmostEqual(result, expected, places=6) + + +class TestRoundtripConversion(unittest.TestCase): + + def setUp(self): + self.width_cm = 140 + self.gsm = 180 + + def test_meter_kg_roundtrip(self): + original = 25.5 + kg = convert_meter_to_kg(original, self.width_cm, self.gsm) + back = convert_kg_to_meter(kg, self.width_cm, self.gsm) + self.assertAlmostEqual(original, back, places=6) + + def test_yard_kg_roundtrip(self): + original = 30.0 + kg = convert_yard_to_kg(original, self.width_cm, self.gsm) + back = convert_kg_to_yard(kg, self.width_cm, self.gsm) + self.assertAlmostEqual(original, back, places=6) + + +class TestEdgeCases(unittest.TestCase): + + def test_zero_value(self): + self.assertEqual(convert_meter_to_yard(0), 0) + self.assertEqual(convert_yard_to_meter(0), 0) + self.assertEqual(convert_meter_to_kg(0, 150, 200), 0) + + def test_large_value(self): + result = convert_meter_to_yard(10000) + self.assertAlmostEqual(result, 10000 / YARD_TO_METER, places=2) + + def test_small_value(self): + result = convert_meter_to_yard(0.001) + self.assertAlmostEqual(result, 0.001 / YARD_TO_METER, places=8) + + +class TestPriceConversion(unittest.TestCase): + + def test_price_per_meter_to_yard(self): + price_per_meter = 10.0 + price_per_yard = price_per_meter * YARD_TO_METER + self.assertAlmostEqual(price_per_yard, 9.144, places=3) + + def test_price_per_kg_to_meter(self): + price_per_kg = 50.0 + width_cm = 150 + gsm = 200 + kg_per_meter = (width_cm / 100) * (gsm / 1000) + price_per_meter = price_per_kg * kg_per_meter + self.assertAlmostEqual(price_per_meter, 15.0, places=6) + + +if __name__ == '__main__': + unittest.main()