@PythonHub:可复用的基于场景的测试框架 一次编写测试,即可在每种产品变体、设备或技术上运行——…
摘要
Balder 是一个 Python 测试框架,允许编写可复用的基于场景的测试,无需重写代码即可在不同设备或技术上运行。
查看缓存全文
缓存时间: 2026/07/28 10:27
可复用场景化测试框架。一次编写测试,即可在所有产品变体、设备或技术上运行——无需重写任何代码。https://t.co/dyOY5sKKnF — # balder-dev/balder 来源:https://github.com/balder-dev/balder Balder 是一个 Python 测试系统,允许你将一次编写的测试代码复用于不同产品版本或变体,而无需任何代码重复。通过将测试逻辑与产品特定实现分离,它使你能够在几分钟内将整个测试套件适配到新设备或新技术上——即使它们使用完全不同的底层机制。这让你可以安装即用型测试用例,并提供各种测试开发功能,帮助更快地测试软件或嵌入式设备。你可以从现有的 BalderHub 项目(https://hub.balder.dev)导入可复用的测试代码(完全可定制),也可以构建自己的代码。参与进来,与你的团队、公司甚至全世界分享你的测试。 # 安装 你可以使用 pip 安装最新版本: python -m pip install baldertest # 运行 Balder 安装完成后,你可以通过以下命令运行 Balder: balder 你也可以通过控制台参数指定 balder 环境目录的路径: balder --working-dir /path/to/working/dir # 它是如何工作的? Balder 通过将测试拆分为两个关键概念来实现测试代码复用: 场景(Scenarios): 定义测试需要什么——通常是抽象的、不含实现细节的业务逻辑。 设置(Setups): 描述你拥有什么——场景中(抽象)特性的具体实现。 测试被编写为场景类中的方法(以 test_* 开头)。Balder 自动解析场景与设置之间的兼容映射,动态生成并运行变体。 ## 定义 Scenario 类 场景使用内部 Device 类来概述所需的设备及其特性。特性是定义接口(例如 switch_on() 等方法)的抽象类。 让我们创建一个包含两个设备的新场景:一个设备发射光,另一个设备检测光: light_expl_scenario.svg 以下是该场景的实现: python import balder from lib.scenario_features import BaseLightSpendingFeature, BaseLightDetectorFeature class ScenarioLight(balder.Scenario): # 该测试所需的设备及其特性 class LightSpendingDevice(balder.Device): light = BaseLightSpendingFeature() @balder.connect(LightSpendingDevice, over_connection=balder.Connection) class LightDetectingDevice(balder.Device): detector = BaseLightDetectorFeature() # 测试方法:必须以 `test_*` 开头,仅在场景中定义 # -> 可以使用所有场景设备特性 def test_check_light(self): self.LightSpendingDevice.light.switch_on() assert self.LightDetectingDevice.detector.light_is_on() self.LightSpendingDevice.light.switch_off() assert not self.LightDetectingDevice.detector.light_is_on() 该场景需要两个设备:一个发射光,一个检测光。测试逻辑保持通用。 BaseLightSpendingFeature:抽象特性,包含 switch_on() 和 switch_off() 等方法。 BaseLightDetectorFeature:抽象特性,包含 light_is_on() 等方法。 两个设备相互连接(通过 @balder.connect(..) 定义),因为发光设备和光检测设备需要以某种方式进行交互。 ## 定义 Setup 类 下一步是定义一个描述我们拥有什么的 Setup 类。要使一个 Scenario 与 Setup 匹配,场景所需的每个特性都必须在设置中对应的映射设备内存在子类。 用于在车库设置 SetupGarage 中测试带有灯的汽车: light_expl_setup.svg 在代码中,如下所示: python import balder from lib.setup_features import CarEngineFeature, CarLightFeature, \ LightDetectorFeature class SetupGarage(balder.Setup): class Car(balder.Device): car_engine = CarEngineFeature() car_light = CarLightFeature() # `BaseLightSpendingFeature` 的子类 ... @balder.connect(Car, over_connection=balder.Connection) class Sensor(balder.Device): detector = LightDetectorFeature() # `BaseLightDetectorFeature` 的子类 注意,CarLightFeature 是 BaseLightSpendingFeature 的子类,LightDetectorFeature 是 BaseLightDetectorFeature 的子类。Balder 通过检查设置设备是否为场景候选设备所需的所有特性提供了实现,来扫描可能的匹配项。当 Balder 识别出一个有效的变体——即每个场景设备都映射到一个实现了所有必要场景特性的设置设备时——它就会使用该变体执行测试。 light_expl_setup_garage_caronly.gif 在我们的例子中,它找到一个匹配的变体(LightSpendingDevice -> Car | LightDetectingDevice -> Sensor)并运行测试。 shell +----------------------------------------------------------------------------------------------------------------------+ | BALDER 测试系统 | | python 版本 3.10.12 (main, Aug 15 2025, 14:32:43) [GCC 11.4.0] | balder 版本 0.1.0b14 | +----------------------------------------------------------------------------------------------------------------------+ 收集到 1 个 Setups 和 1 个 Scenarios 解析出 1 个有效变体 ================================================== 开始测试会话 =================================================== SETUP SetupGarage SCENARIO ScenarioLight VARIATION ScenarioLight.LightSpendingDevice:SetupGarage.Car | ScenarioLight.LightDetectingDevice:SetupGarage.Sensor TEST ScenarioLight.test_check_light [.] ================================================== 完成测试会话 ================================================== 总 NOT_RUN: 0 | 总 FAILURE: 0 | 总 ERROR: 0 | 总 SUCCESS: 1 | 总 SKIP: 0 | 总 COVERED_BY: 0 ## 向 Setup 类添加另一个设备 现在 Balder 的巨大优势就体现出来了。我们可以用所有能实现 BaseLightSpendingFeature 的设备来运行测试,无论其具体实现方式如何。你无需重写测试。 所以,我们的车库里有更多设备。让我们添加它们: python import balder from lib.setup_features import CarEngineFeature, CarLightFeature, \ PedalFeature, BicycleLightFeature, \ GateOpenerFeature, \ LightDetectorFeature class SetupGarage(balder.Setup): class Car(balder.Device): car_engine = CarEngineFeature() car_light = CarLightFeature() # `BaseLightSpendingFeature` 的子类 ... class Bicycle(balder.Device): pedals = PedalFeature() light = BicycleLightFeature() # `BaseLightSpendingFeature` 的另一个子类 class GarageGate(balder.Device): opener = GateOpenerFeature() @balder.connect(Car, over_connection=balder.Connection) @balder.connect(Bicycle, over_connection=balder.Connection) class Sensor(balder.Device): detector = LightDetectorFeature() # `BaseLightDetectorFeature` 的子类 BicycleLightFeature 的实现可能与 CarLightFeature 完全不同,但由于它始终为 BaseLightSpendingFeature 中的抽象方法提供实现,因此测试可以在两种变体下执行。 Balder 现在检测到两个变体(以 Car 和 Bicycle 作为光源): light_expl_setup_garage_full.gif 运行 Balder 的结果如下所示: shell +----------------------------------------------------------------------------------------------------------------------+ | BALDER 测试系统 | | python 版本 3.10.12 (main, Aug 15 2025, 14:32:43) [GCC 11.4.0] | balder 版本 0.1.0b14 | +----------------------------------------------------------------------------------------------------------------------+ 收集到 1 个 Setups 和 1 个 Scenarios 解析出 2 个有效变体 ================================================== 开始测试会话 =================================================== SETUP SetupGarage SCENARIO ScenarioLight VARIATION ScenarioLight.LightSpendingDevice:SetupGarage.Bicycle | ScenarioLight.LightDetectingDevice:SetupGarage.Sensor TEST ScenarioLight.test_check_light [.] VARIATION ScenarioLight.LightSpendingDevice:SetupGarage.Car | ScenarioLight.LightDetectingDevice:SetupGarage.Sensor TEST ScenarioLight.test_check_light [.] ================================================== 完成测试会话 ================================================== 总 NOT_RUN: 0 | 总 FAILURE: 0 | 总 ERROR: 0 | 总 SUCCESS: 2 | 总 SKIP: 0 | 总 COVERED_BY: 0 ScenarioLight 中的测试实现没有改变,但执行将分别使用 CarLightFeature 和 BicycleLightFeature! ## 使用另一个光传感器 你有另一个测试设置,使用另一种方法来检查灯是否已通电?将 LightDetectorFeature 替换为新的 MeasureLightByVoltageFeature(它也是 BaseLightDetectorFeature 的子类): python import balder from lib.setup_features import CarEngineFeature, CarLightFeature, \ PedalFeature, BicycleLightFeature, \ GateOpenerFeature, \ MeasureLightByVoltageFeature class SetupLaboratory(balder.Setup): class Car(balder.Device): car_engine = CarEngineFeature() car_light = CarLightFeature() # `BaseLightSpendingFeature` 的子类 ... class Bicycle(balder.Device): pedals = PedalFeature() light = BicycleLightFeature() # `BaseLightSpendingFeature` 的另一个子类 class GarageGate(balder.Device): opener = GateOpenerFeature() @balder.connect(Car, over_connection=balder.Connection) @balder.connect(Bicycle, over_connection=balder.Connection) class Sensor(balder.Device): detector = MeasureLightByVoltageFeature() # `BaseLightDetectorFeature` 的另一个子类 当 Balder 执行时,它会同时执行两种设置:一次通过传感器测量光(SetupGarage),另一次通过电压测量检测光(SetupLaboratory)。 shell +----------------------------------------------------------------------------------------------------------------------+ | BALDER 测试系统 | | python 版本 3.10.12 (main, Aug 15 2025, 14:32:43) [GCC 11.4.0] | balder 版本 0.1.0b14 | +----------------------------------------------------------------------------------------------------------------------+ 收集到 2 个 Setups 和 1 个 Scenarios 解析出 4 个有效变体 ================================================== 开始测试会话 =================================================== SETUP SetupGarage SCENARIO ScenarioLight VARIATION ScenarioLight.LightSpendingDevice:SetupGarage.Bicycle | ScenarioLight.LightDetectingDevice:SetupGarage.Sensor TEST ScenarioLight.test_check_light [.] VARIATION ScenarioLight.LightSpendingDevice:SetupGarage.Car | ScenarioLight.LightDetectingDevice:SetupGarage.Sensor TEST ScenarioLight.test_check_light [.] SETUP SetupLaboratory SCENARIO ScenarioLight VARIATION ScenarioLight.LightSpendingDevice:SetupLaboratory.Bicycle | ScenarioLight.LightDetectingDevice:SetupLaboratory.Sensor TEST ScenarioLight.test_check_light [.] VARIATION ScenarioLight.LightSpendingDevice:SetupLaboratory.Car | ScenarioLight.LightDetectingDevice:SetupLaboratory.Sensor TEST ScenarioLight.test_check_light [.] ================================================== 完成测试会话 ================================================== 总 NOT_RUN: 0 | 总 FAILURE: 0 | 总 ERROR: 0 | 总 SUCCESS: 4 | 总 SKIP: 0 | 总 COVERED_BY: 0 Balder 会为你处理所有这些。你只需通过定义 Scenario 和 Setup 类来描述你的环境,并通过创建设置级特性来提供具体的实现。Balder 自动确定适用的变体并运行测试。 注意: Balder 提供了更多元素来设计完整的设备结构,包括设备之间的连接。你可以在文档的教程部分(https://docs.balder.dev/en/latest/tutorial_guide/index.html)了解更多信息。 # 示例:使用可安装的 BalderHub 包 使用 Balder,你可以创建自定义测试环境,或安装开源可用的测试包,即 BalderHub 包(https://hub.balder.dev)。例如,如果你想测试网站的登录功能,只需使用来自 balderhub-auth 包(https://hub.balder.dev/projects/auth/en/latest/examples.html)的即用型场景 ScenarioSimpleLogin。 如果你想使用 Selenium(https://www.selenium.dev/)来控制浏览器,当然也使用 html 元素,你可以直接安装 balderhub-selenium 和 balderhub-html。 $ pip install balderhub-auth balderhub-selenium balderhub-html 无需编写自己的测试场景,你可以直接导入一个即用型场景: python # 文件 `scenario_balderhub.py` from balderhub.auth.scenarios import ScenarioSimpleLogin 根据该 BalderHub 项目的文档(https://hub.balder.dev/projects/auth/en/latest/examples.html),我们只需通过重写 LoginPage 特性来定义登录页面: python # 文件 `lib/pages.py` import balderhub.auth.contrib.html.pages from balderhub.html.lib.utils import Selector from balderhub.url.lib.utils import Url import balderhub.html.lib.utils.components as html class LoginPage(balderhub.auth.contrib.html.pages.LoginPage): url = Url('https://example.com') # 重写抽象属性 @property def input_username(self): return html.inputs.HtmlTextInput.by_selector(self.driver, Selector.by_name('user')) # 重写抽象属性 @property def input_password(self): return html.inputs.HtmlPasswordInput.by_selector(self.driver, Selector.by_name('user')) # 重写抽象属性 @property def btn_login(self): return html.HtmlButtonElement.by_selector(self.driver, Selector.by_id('submit-button')) 然后在我们的设置中使用它: python # 文件 `setups/setup_office.py` import balder import balderhub.auth.lib.scenario_features.role from balderhub.selenium.lib.setup_features import SeleniumChromeWebdriverFeature from lib.pages import LoginPage from lib.setup_features import UserConfig # 提供用户登录数据的另一个特性 class SetupOffice(balder.Setup): class Server(balder.Device): user = UserConfig() class Browser(balder.Device): selenium = SeleniumChromeWebdriverFeature() page_login = LoginPage() # fixture 用于准备 selenium - 将在测试会话运行前执行 @balder.fixture('session') def selenium(self): self.Browser.selenium.create() yield self.Browser.selenium.quit() 现在当你运行 Balder 时,它将执行一个完整的登录测试,而这个测试并非你自己编写的——它是由开源社区创建的。 shell +----------------------------------------------------------------------------------------------------------------------+ | BALDER 测试系统 | | python 版本 3.10.12 (main, Aug 15 2025, 14:32:43) [GCC 11.4.0] | balder 版本 0.1.0b14 | +----------------------------------------------------------------------------------------------------------------------+ 收集到 1 个 Setups 和 1 个 Scenarios 解析出 1 个有效变体 ================================================== 开始测试会话 =================================================== SETUP SetupOffice SCENARIO ScenarioSimpleLogin VARIATION ScenarioSimpleLogin.Client:SetupOffice.Browser | ScenarioSimpleLogin.System:SetupOffice.Server TEST ScenarioSimpleLogin.test_login [.] ================================================== 完成测试会话 ================================================== 总 NOT_RUN: 0 | 总 FAILURE: 0 | 总 ERROR: 0 | 总 SUCCESS: 1 | 总 SKIP: 0 | 总 COVERED_BY: 0 如果你想了解更多信息,欢迎深入阅读文档(https://balder.dev)。 # 贡献指南 欢迎任何形式的帮助。如果你想为 balder 做出贡献,请查看贡献指南(https://balder.dev/contribute)。
相似文章
上下文使测试更具可重用性
作者分享了在Guile中设计测试框架的经验,重点探讨了向测试定义添加上下文如何使测试更可重用并改善开发者体验。
现在需要运行五个 Python 类型检查器吗?
一篇博文主张 Python 库维护者应优先在测试套件中运行多个类型检查器,以确保公共 API 的兼容性,并强调了兼容性和代码污染方面的挑战。
@RayFernando1337: 我发现最新的模型写了很多单元测试。其中大多数只是重复了它们之后编写的代码,所以它们总是…
一条推文批评AI模型编写无效的单元测试,并分享了一个提示来加强更好的测试实践,强调E2E测试,避免在代码之后编写单元测试。
TryCase
TryCase 提供专为AI编码代理设计的一次性测试环境,确保安全且隔离的测试。
可编程的属性测试
本文讨论了可编程的属性测试,这是一种软件测试技术,允许根据定义的属性自定义生成测试用例。