"""commands/unit_test.py 单元测试 — cmd_unit_test + _parse_duration。 运行: python tests/unit/test_unit_test.py """ from __future__ import annotations import argparse import os import sys import unittest from unittest.mock import MagicMock, patch sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "assets")) def _args(**kwargs): defaults = {"type": "class", "name": "ZCL_TEST"} defaults.update(kwargs) return argparse.Namespace(**defaults) # ═══════════════════════════════════════════ # _parse_duration # ═══════════════════════════════════════════ class TestParseDuration(unittest.TestCase): """_parse_duration 安全解析方法耗时。""" def test_valid_float_string(self): from sapcli.commands.unit_test import _parse_duration self.assertEqual(_parse_duration("1.5"), 1.5) self.assertEqual(_parse_duration("0.25"), 0.25) self.assertEqual(_parse_duration("3"), 3.0) def test_invalid_string_returns_zero(self): from sapcli.commands.unit_test import _parse_duration self.assertEqual(_parse_duration("abc"), 0.0) def test_none_returns_zero(self): from sapcli.commands.unit_test import _parse_duration self.assertEqual(_parse_duration(None), 0.0) def test_empty_string_returns_zero(self): from sapcli.commands.unit_test import _parse_duration self.assertEqual(_parse_duration(""), 0.0) def test_zero_string(self): from sapcli.commands.unit_test import _parse_duration self.assertEqual(_parse_duration("0"), 0.0) # ═══════════════════════════════════════════ # cmd_unit_test # ═══════════════════════════════════════════ class TestCmdUnitTest(unittest.TestCase): """cmd_unit_test 执行 ABAP Unit 测试并汇总结果。""" def test_api_error_prints_failure_and_returns(self): """run_unit_test 抛异常时打印失败提示并返回(不抛)。""" from sapcli.commands.unit_test import cmd_unit_test client = MagicMock() client.run_unit_test.side_effect = Exception("connection refused") with patch("builtins.print") as mock_print: cmd_unit_test(_args(), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("失败", printed) def test_no_test_classes(self): """无测试类时打印提示。""" from sapcli.commands.unit_test import cmd_unit_test client = MagicMock() client.run_unit_test.return_value = {"summary": {}, "classes": []} with patch("builtins.print") as mock_print: cmd_unit_test(_args(), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("未找到测试类", printed) def test_passed_class(self): """全部通过时打印 PASSED 和方法数。""" from sapcli.commands.unit_test import cmd_unit_test client = MagicMock() client.run_unit_test.return_value = { "summary": { "tests": "2", "failures": "0", "errors": "0", "skipped": "0", }, "classes": [ { "name": "ZCL_TEST", "methods": [ {"name": "test1", "duration": "1.5"}, {"name": "test2", "duration": "0.5"}, ], }, ], } with patch("builtins.print") as mock_print: cmd_unit_test(_args(), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("PASSED", printed) self.assertIn("2 methods", printed) # 合计行 self.assertIn("合计", printed) def test_failed_class_with_alert_and_line(self): """方法失败时打印 FAILED、方法名、行号、告警内容。""" from sapcli.commands.unit_test import cmd_unit_test client = MagicMock() client.run_unit_test.return_value = { "summary": { "tests": "1", "failures": "1", "errors": "0", "skipped": "0", }, "classes": [ { "name": "ZCL_TEST", "methods": [ { "name": "test_fail", "duration": "0.1", "alert": "Assertion failed", "line": "42", }, ], }, ], } with patch("builtins.print") as mock_print: cmd_unit_test(_args(), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("FAILED", printed) self.assertIn("test_fail", printed) self.assertIn("42", printed) self.assertIn("Assertion failed", printed) self.assertIn("at line 42", printed) def test_failed_without_line_omits_line_info(self): """失败方法无 line 时不打印 at line。""" from sapcli.commands.unit_test import cmd_unit_test client = MagicMock() client.run_unit_test.return_value = { "summary": {}, "classes": [ { "name": "ZCL_TEST", "methods": [{"name": "test_x", "alert": "boom"}], }, ], } with patch("builtins.print") as mock_print: cmd_unit_test(_args(), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("FAILED", printed) self.assertIn("boom", printed) self.assertNotIn("at line", printed) def test_class_without_name_falls_back_to_display(self): """class name 为空时回退到 parsed.display_name。""" from sapcli.commands.unit_test import cmd_unit_test client = MagicMock() client.run_unit_test.return_value = { "summary": {}, "classes": [ {"name": None, "methods": [{"name": "test1"}]}, ], } with patch("builtins.print") as mock_print: cmd_unit_test(_args(name="ZCL_FOO"), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) self.assertIn("ZCL_FOO", printed) def test_summary_defaults_when_missing(self): """summary 缺字段时使用默认值(? / 0)。""" from sapcli.commands.unit_test import cmd_unit_test client = MagicMock() client.run_unit_test.return_value = { "summary": {}, "classes": [{"name": "ZCL", "methods": [{"name": "t1"}]}], } with patch("builtins.print") as mock_print: cmd_unit_test(_args(), client) printed = " ".join(str(c) for c, _ in mock_print.call_args_list) # tests 默认 "?" self.assertIn("?", printed) if __name__ == "__main__": unittest.main(verbosity=2)