从单元测试到传感器应用: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
-
配置测试文件
:
-
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
-
配置
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;
通过这个流程图,我们可以清晰地看到整个项目的开发过程,以及各个步骤之间的关系。在实际开发中,我们可以根据这个流程来组织开发工作,确保项目的顺利进行。
超级会员免费看
5万+

被折叠的 条评论
为什么被折叠?



