Skip to content

[Quality] 测试覆盖率: ZERO Test Coverage - Critical Quality Gap #11

@newtontech

Description

@newtontech

🚨 实际问题

文件: DAM3000M.py - 完全无测试覆盖

测试状态: 0% 覆盖率 - 没有任何测试文件

该仓库包含 288 行硬件控制代码,但完全没有测试:

  • ❌ 没有 test_*.py 文件
  • ❌ 没有 tests/ 目录
  • ❌ 没有 pytest.ini / pyproject.toml 测试配置
  • ❌ 没有 CI/CD 测试流程

问题 1: 模块导入时加载DLL (阻断测试)

文件: DAM3000M.py:75-76
代码:

dll_path = "./DAM3000M_64.dll"
dam3000m = ctypes.WinDLL(dll_path)

问题:

  • DLL 在模块导入时就加载(全局作用域)
  • 硬编码的相对路径
  • Windows-only 依赖无条件加载
  • 导致单元测试无法运行 - 导入就会失败

问题 2: 硬件紧耦合 (无法Mock)

文件: DAM3000M.py:123-137
代码:

class DAMDevice:
    handles={}  # 类级别可变状态
    def __init__(self, com_id: int, baud_rate: int, device_id:int):
        self.handle=self._get_handle()
    def _get_handle(self):
        if self.com_id not in self.handles:
            handle=DAM3000M_CreateDevice(self.com_id)  # 直接硬件调用
            assert handle not in (-1,None, 0)
            assert DAM3000M_InitDevice(handle, ...)

问题:

  • 没有 DLL 函数的抽象层
  • 无法在测试中模拟硬件响应
  • 使用断言进行错误处理(不利于测试)
  • 类级别 handles 字典导致测试隔离问题

问题 3: exit 方法签名错误

文件: DAM3000M.py:146-147
代码:

def __exit__(self):
    assert DAM3000M_ReleaseDevice(self.handle),"Failed to release device."

问题:

  • __exit__ 需要 4 个参数: (self, exc_type, exc_val, exc_tb)
  • 当前实现破坏了上下文管理器协议
  • 无法使用 with 语句

问题 4: 死代码

文件: DAM3000M.py:224-254
代码:

# DAM3151_device=DAM3151(4,3,5)
# data=DAM3151_device.measure_all_channels_mA_V()
# print(data)

# class ChannelController:
#     DAM3060V_device_id_list=[1,2,3,4]
#     ...

问题: 大量注释掉的代码块降低可维护性


🔧 改进建议

第一步: 重构使代码可测试

# 将DLL加载改为延迟加载
class DllInterface:
    _instance = None
    
    @classmethod
    def get_instance(cls, dll_path: str = "./DAM3000M_64.dll"):
        if cls._instance is None:
            cls._instance = ctypes.WinDLL(dll_path)
        return cls._instance
    
    @classmethod
    def set_mock(cls, mock_instance):
        """用于测试时注入mock"""
        cls._instance = mock_instance

# 修复__exit__签名
class DAMDevice:
    def __exit__(self, exc_type, exc_val, exc_tb):
        DAM3000M_ReleaseDevice(self.handle)
        return False  # 不抑制异常

第二步: 添加测试文件 tests/test_dam3000m.py

import pytest
from unittest.mock import Mock, patch, MagicMock
import sys
sys.path.insert(0, '.')

class TestDAM3060V:
    """测试DAM3060V的模拟输出功能"""
    
    def test_range_modes_defined(self):
        """测试量程模式定义正确"""
        from DAM3000M import DAM3060V
        assert 9 in DAM3060V.RangeModes  # -10V ~ 10V
        assert 8 in DAM3060V.RangeModes  # -5V ~ 5V
        assert 14 in DAM3060V.RangeModes  # 0V ~ 10V
        assert 13 in DAM3060V.RangeModes  # 0V ~ 5V
    
    def test_voltage_to_lsb_calculation(self):
        """测试电压到LSB的转换计算"""
        from DAM3000M import DAM3060V
        # mode 8: -5V ~ 5V
        range_bottom, range_top = -5, 5
        value = 0  # 中点
        dal_lsb = ceil((value - range_bottom) * 0xFFF / (range_top - range_bottom))
        assert dal_lsb == 0x7FF  # 约等于一半

class TestDAM3151:
    """测试DAM3151的测量功能"""
    
    def test_data_converter(self):
        """测试原始数据转换为mA/V"""
        from DAM3000M import DAM3151
        device = object.__new__(DAM3151)  # 不调用__init__
        device.fLsbType = 65535.0
        
        # 测试0-20mA量程 (mode 11)
        raw_data = 32767  # 中点
        result = device._data_converter(raw_data, 20, 0)
        assert abs(result - 10.0) < 0.1  # 约等于10mA
    
    def test_channel_enable_mask(self):
        """测试通道使能掩码计算"""
        from DAM3000M import DAM3151
        assert (1 << 32) - 1 == 0xFFFFFFFF  # 所有32通道使能

class TestDeviceInfo:
    """测试设备信息结构"""
    
    def test_device_info_structure(self):
        """测试DeviceInfo结构体定义"""
        from DAM3000M import DeviceInfo
        info = DeviceInfo()
        # 验证所有字段存在
        assert hasattr(info, 'DeviceType')
        assert hasattr(info, 'TypeSuffix')
        assert hasattr(info, 'ModusType')

第三步: 添加测试配置 pyproject.toml

[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "art-control-python-interface"
version = "0.1.0"
description = "阿尔泰科技DAM3000M系列控制代码Python接口"
dependencies = []

[project.optional-dependencies]
test = [
    "pytest>=7.0",
    "pytest-cov>=4.0",
    "pytest-mock>=3.0"
]

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
    "--cov=DAM3000M",
    "--cov-report=html",
    "--cov-report=term-missing",
    "--cov-fail-under=50"
]

[tool.coverage.run]
source = ["DAM3000M.py"]
omit = ["tests/*"]

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise AssertionError",
    "raise NotImplementedError",
]

第四步: 添加GitHub Actions CI .github/workflows/test.yml

name: Tests

on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]

jobs:
  test:
    runs-on: windows-latest  # 需要Windows环境
    steps:
    - uses: actions/checkout@v3
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.9'
    - name: Install dependencies
      run: |
        pip install -e ".[test]"
    - name: Run tests with coverage
      run: |
        pytest
    - name: Upload coverage report
      uses: actions/upload-artifact@v3
      with:
        name: coverage-report
        path: htmlcov/

📊 为什么重要

风险项 影响
无回归保护 任何代码修改都可能破坏硬件控制功能
无法安全重构 改进代码结构时无法验证正确性
硬件依赖测试 每次测试都需要实际硬件连接
CI/CD阻塞 无法建立自动化发布流程
协作困难 其他开发者无法验证其修改

🎯 优先级

  • - 影响系统稳定性/安全性 - 当前为0%覆盖率,属于高风险

建议行动:

  1. 立即添加基础测试框架配置
  2. 重构代码使硬件依赖可注入/可mock
  3. 为核心计算逻辑(如 _data_converter, set_analog_output)添加单元测试
  4. 建立CI/CD自动化测试流程

📈 目标指标

  • 核心计算逻辑测试覆盖率 ≥ 80%
  • 所有硬件交互可mock
  • CI流水线自动化测试通过
  • 无导入时副作用

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions