14、从单元测试到传感器应用:Python 项目实践与技巧

从单元测试到传感器应用:Python 项目实践与技巧

一、单元测试入门

在软件开发中,测试驱动开发(TDD)是一种有效的方法,即先编写单元测试,再编写应用逻辑,这样能产出更优质的代码。不过在之前的项目里,由于大家还在熟悉 Python、Flask 和应用本身,所以暂时没有编写单元测试。从现在开始,我们要着手编写第一个单元测试。

首先,需要明确哪些部分需要进行单元测试。单元测试主要用于测试小型独立组件。像 data.py 里的 Flask 路由,逻辑较少,更适合进行集成测试,因为它最终是将数据展示给外部; app.py 除了 Flask 配置和日志记录外,也没太多可单元测试的内容。而且一般可以信赖第三方库能正常工作,无需对其进行测试。

因此, sensors.py 成为了编写单元测试的核心文件,它实现了业务逻辑。其中, get_external_temp 函数由于测试其少量逻辑的难度远大于收益,所以在本项目中暂不进行测试;而 get_boot_status 函数则非常适合,它有自己的逻辑和几个简单条件,对第三方逻辑的依赖也容易在测试中替换。

get_boot_status 函数返回启动传感器的状态,有三种可能情况:
- Open
- Closed
- Unknown

这些状态由错误处理逻辑或 button.is_pressed 的输出决定。为了在测试中严格控制条件,需要了解模拟(mocking)的概念。模拟就是替换代码中的某个组件,这个组件在当前测试中并非被测试的部分,比如昂贵的 API 调用或危险的函数。模拟外部依赖是单元测试的重要部分。

二、使用 Pytest 框架进行单元测试

我们将使用 Pytest 单元测试框架,它是旧的 unittest 库的扩展。虽然两者都是优秀且常用的选择,但 Pytest 正迅速成为 Python 单元测试的首选库。没有像 Pytest 这样的框架也能进行单元测试,但会更麻烦,因为 Pytest 能处理测试运行、确保测试通过、显示失败的错误信息等。

以下是具体的操作步骤:
1. 清理测试文件夹 :在 tests 文件夹中,删除 test_practice.py 文件,因为现在要编写真正的测试,这个假测试不再需要。
2. 创建测试文件
- 创建 test_sensors.py 文件,该文件将包含 sensors 类的所有测试:

touch tests/test_sensors.py
- 创建 `tests/conftest.py` 和 `Pi_Car/config/test_config.py` 文件:
touch tests/conftest.py
touch Pi_Car/config/test_config.py
  1. 配置测试文件
    • test_config.py 文件用于存储应用运行测试时的配置:
TESTING = True
LOGGER_LEVEL = "DEBUG"
FILE_LOGGING = False
- 在 `local_config.py` 中添加新的日志选项:
FILE_LOGGING = True
- 在 `app.py` 的 `create_app` 函数中实现该配置:
def create_app(config_file="config/local_config.py"):
    app = Flask(__name__)  # Initialize app
    app.config.from_pyfile(config_file, silent=False)  # Read in config from file
    if app.config.get("FILE_LOGGING"):
        # Configure file based log handler
        log_file_handler = RotatingFileHandler(
            filename=app.config.get("LOG_FILE_NAME", "config/pi-car.log"),
            maxBytes=10000000,
            backupCount=4,
        )
        log_file_handler.setFormatter(
            logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
        )
        app.logger.addHandler(log_file_handler)
    app.logger.setLevel(app.config.get("LOGGER_LEVEL", "ERROR"))
    app.logger.info("----- STARTING APP ------")
    app.register_blueprint(data_blueprint)
    app.logger.info("----- FINISHED STARTING APP -----")
    return app
  1. 配置 conftest.py 文件
import pytest
from Pi_Car.app import create_app

@pytest.fixture(scope="session", autouse=True)
def build_testing_app():
    """
    Builds a testing app with basic application context
    """
    app = create_app(config_file="config/test_config.py")
    app.app_context().push()
    yield app

这里的 build_testing_app 函数是一个 Pytest 夹具,用于构建一个带有基本应用上下文的测试应用。 yield 语句的作用是在测试开始时构建并返回 Flask 应用,测试结束后可以执行后续代码,这样能减少样板代码。

三、编写 test_sensors.py 中的测试函数

test_sensors.py 中,有四个测试函数,每个函数测试 get_boot_status 函数的一个特定条件:

from unittest.mock import patch
from Pi_Car.sensors import Sensors
from gpiozero import exc

class TestSensors:
    @patch("Pi_Car.sensors.Button")
    def test_get_boot_status_bad_pin_factory(self, mock_button):
        mock_button.side_effect = exc.BadPinFactory
        result = Sensors.get_boot_status()
        assert result == "Unknown"

    @patch("Pi_Car.sensors.Button")
    def test_get_boot_status_other_pin_error(self, mock_button):
        mock_button.side_effect = TypeError
        result = Sensors.get_boot_status()
        assert result == "Unknown"

    @patch("Pi_Car.sensors.Button")
    def test_get_boot_status_closed(self, mock_button):
        mock_button.return_value = type("Button", (), {"is_pressed": True})
        result = Sensors.get_boot_status()
        assert result == "Closed"

    @patch("Pi_Car.sensors.Button")
    def test_get_boot_status_open(self, mock_button):
        mock_button.return_value = type("Button", (), {"is_pressed": False})
        result = Sensors.get_boot_status()
        assert result == "Open"

这些测试函数的执行流程如下:

graph TD;
    A[开始测试] --> B[配置模拟条件];
    B --> C[运行被测试函数];
    C --> D[使用 assert 语句验证结果];
    D --> E[结束测试];

可以在控制台使用以下命令运行这些测试:

pytest

如果想查看日志,可以使用 -s 标志:

pytest -s

Pytest 会自动运行 test 文件夹中以 test 开头的文件、类和函数作为测试。测试成功时,Pytest 状态条为绿色;有失败时,状态条为红色并指出失败数量和条件。

四、模拟和断言的使用

每个测试都使用 @patch("Pi_Car.sensors.Button") 装饰器来模拟传感器库。前两个测试函数使用 side_effect 属性让代码抛出特定错误,确保在两种错误条件下函数返回 “Unknown”。后两个测试使用 return_value 属性强制 Button 类的 is_pressed 属性返回特定值。

断言(assert)是 Python 中的特殊工具,用于确保条件符合标准。例如:

result = Sensors.get_boot_status()
assert result == "Open"

如果条件不满足,测试将失败。需要注意的是,断言只能在测试代码中使用,不能用于生产代码,因为 Python 可以禁用断言,禁用后依赖断言的代码会失败。

五、光传感器项目介绍

接下来要进行一个光传感器项目,目标是安装光传感器并将其状态读取到应用中,同时开始测试驱动开发,提升单元测试技能。

这个项目使用光传感器读取环境光,判断是白天还是夜晚。它不仅能告知车内人员外面天色渐暗,还为未来项目打下基础,比如自动打开车头灯或在黑暗中开门时打开车内灯。

六、光传感器硬件配置

在连接或断开任何电路时,记得断开 Pi 的电源供应。完成这个项目需要一个光敏电阻(LDR),它也被称为光感电阻、光敏依赖电阻。LDR 价格便宜,在电子商店很容易买到。项目不需要独立电阻,将 LDR 连接到面包板上的 Pi 即可。

LDR 的工作原理类似于电阻,能限制电流。特殊之处在于其电阻会根据环境光的强度变化,光越强,电阻越大;光越弱,电阻越小。Pi 可以检测电阻变化,并将其转换为 0(完全黑暗)到 1(明亮白天)之间的信号。

通过以上步骤,我们从单元测试入门,逐步深入到传感器应用项目,掌握了使用 Pytest 进行单元测试的方法和模拟、断言的技巧,同时了解了光传感器项目的基本情况和硬件配置。

从单元测试到传感器应用:Python 项目实践与技巧

七、测试驱动开发在光传感器项目中的应用

在光传感器项目中,我们将采用测试驱动开发(TDD)的方法,即先编写单元测试,再编写应用代码。这种方法有助于提高代码质量,确保新功能的实现不会破坏现有的功能。

在开始编写测试之前,我们需要先了解光传感器的工作原理和可能的输出状态。光传感器通过检测环境光的强度来判断是白天还是夜晚,因此我们可以根据这个特性来定义测试用例。

以下是使用 Pytest 的 parametrize 装饰器来快速测试多个条件的示例:

import pytest
from Pi_Car.light_sensor import LightSensor

@pytest.mark.parametrize("light_intensity, expected_result", [
    (0.1, "Night"),
    (0.9, "Day"),
    (0.5, "Unknown")
])
def test_light_sensor_status(light_intensity, expected_result):
    sensor = LightSensor()
    result = sensor.get_status(light_intensity)
    assert result == expected_result

在这个示例中, parametrize 装饰器接受两个参数:一个是参数名称的字符串,另一个是包含参数值和预期结果的元组列表。Pytest 会自动为每个元组生成一个测试用例,这样我们就可以快速测试多个条件。

八、避免测试代码重复

在编写单元测试时,我们可能会遇到一些重复的代码,例如初始化对象、设置模拟条件等。为了避免代码重复,我们可以使用 Pytest 的夹具(fixture)来共享代码。

以下是一个使用夹具的示例:

import pytest
from Pi_Car.light_sensor import LightSensor

@pytest.fixture
def light_sensor():
    return LightSensor()

def test_light_sensor_initialization(light_sensor):
    assert isinstance(light_sensor, LightSensor)

def test_light_sensor_status(light_sensor):
    result = light_sensor.get_status(0.1)
    assert result == "Night"

在这个示例中, light_sensor 是一个夹具函数,它返回一个 LightSensor 对象。在测试函数中,我们可以通过参数来使用这个夹具,这样就可以避免在每个测试函数中重复初始化对象。

九、光传感器代码实现

在编写完单元测试后,我们可以开始编写光传感器的应用代码。以下是一个简单的实现示例:

class LightSensor:
    def get_status(self, light_intensity):
        if light_intensity < 0.2:
            return "Night"
        elif light_intensity > 0.8:
            return "Day"
        else:
            return "Unknown"

这个代码实现了一个简单的光传感器类,根据输入的光强度返回相应的状态。在实际应用中,我们可以根据具体需求进行扩展和优化。

十、总结与展望

通过以上步骤,我们完成了光传感器项目的开发,包括硬件配置、单元测试和应用代码的实现。在这个过程中,我们学习了如何使用测试驱动开发的方法来提高代码质量,如何使用 Pytest 的 parametrize 装饰器和夹具来避免测试代码重复。

未来,我们可以进一步扩展这个项目,例如添加更多的传感器、实现传感器数据的存储和分析等。同时,我们也可以将这些单元测试和开发技巧应用到其他项目中,提高开发效率和代码质量。

以下是整个项目的开发流程总结:
| 步骤 | 描述 |
| ---- | ---- |
| 1 | 确定项目需求,选择合适的传感器 |
| 2 | 进行硬件配置,连接传感器到开发板 |
| 3 | 采用测试驱动开发的方法,编写单元测试 |
| 4 | 根据单元测试编写应用代码 |
| 5 | 使用 Pytest 运行测试,确保代码质量 |
| 6 | 对项目进行扩展和优化,添加更多功能 |

graph LR;
    A[项目需求分析] --> B[硬件配置];
    B --> C[编写单元测试];
    C --> D[编写应用代码];
    D --> E[运行测试];
    E --> F{测试是否通过};
    F -- 是 --> G[项目扩展和优化];
    F -- 否 --> C;

通过这个流程图,我们可以清晰地看到整个项目的开发过程,以及各个步骤之间的关系。在实际开发中,我们可以根据这个流程来组织开发工作,确保项目的顺利进行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值