框架介绍
为了复用原有 Android 端和 iOS 端的 feature 用例,rust 下也选择 cucumber 框架做为单元测试框架。这一选择允许我们无缝地将已有的测试用例迁移至Rust环境中执行。
Cucumber是一种行为驱动开发(BDD)的工具,允许开发人员利用自然语言描述功能测试用例。这些测试用例按特性(Features)组织,并通过一系列场景(Scenarios)详述,每个场景由数个操作步骤(Steps)构成。
rust 下的 cucumber 框架为cucumber,该库为异步测试提供了良好支持。
为了实现接口的模拟测试,推荐使用mockall库,它是Rust中的一个模拟框架,类似于Android的Mockito 以及PowerMock,和iOS的OCMock,支持广泛的模拟功能。
结合Cucumber和mockall,我们能够高效地编写Rust库的单元测试。
Rust Cucumber 介绍
配置 Cucumber
首先,在
Cargo.toml
文件的
[dev-dependencies]
部分加入以下依赖:
ini
复制代码
[dev-dependencies] cucumber = "0.21.0" tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread", "time"] } [[test]] name = "example" # this should be the same as the filename of your test target harness = false # allows Cucumber to print output instead of libtest
Cucumber框架在Rust中是异步实现的,因此需引入
tokio
框架以支持异步执行Cucumber的
run
方法。
测试文件需要放于项目的
tests
目录下。创建
tests/example.rs
文件,并在里面定义测试的入口点。
Rust Cucumber 实例解析
我们在 example.rs中,写一个完整的单元测试实例:
rust
复制代码
#[derive(Debug, Default)] pub struct AirCondition { pub temperature: i8, } impl AirCondition { pub fn new(temperature: i8) -> Self { AirCondition { temperature } } pub fn adjust_temperature(&mut self, value: i8) { self.temperature += value; } pub fn current_temperature(&self) -> i8 { self.temperature } } use cucumber::{given, then, when, World}; use cucumber_demo::AirCondition; use futures::FutureExt; use tokio::time; #[derive(Debug, Default, World)] #[world(init = Self::new)] struct MyWorld { air_condition: AirCondition, } impl MyWorld { fn new() -> Self { MyWorld { air_condition: AirCondition::new(25), } } } #[given(expr = "空调当前温度为:{int}")] fn set_temperature(world: &mut MyWorld, temperature: i8) { world.air_condition = AirCondition::new(temperature); } #[when(expr = "调整温度为:{int}")] fn adjust_temperature(world: &mut MyWorld, value: i8) { world.air_condition.adjust_temperature(value); } #[then(expr = "当前温度应该为:{int}")] fn check_temperature(world: &mut MyWorld, expected_temperature: i8) { assert_eq!( world.air_condition.current_temperature(), expected_temperature ); } #[tokio::main] async fn main() { MyWorld::cucumber() .before(|_feature, _rule, _scenario, _world| { time::sleep(time::Duration::from_millis(1000)).boxed_local() }) .run_and_exit("tests/features/air.feature") .await }
添加 tests/features/air.feature到工程
vbnet
复制代码
Feature: air feature Scenario: 测试空调温度调节1 Given 空调当前温度为:20 When 调整温度为:-2 Then 当前温度应该为:18 Scenario: 测试空调温度调节2 Given 空调当前温度为:21 When 调整温度为:2 Then 当前温度应该为:23 Scenario: 测试空调温度调节3 Given 空调当前温度为:22 When 调整温度为:-3 Then 当前温度应该为:19
在 VS Code 中运行结果如下:
下面逐行讲解一下这个示例:
1 - 16 行:定义了一个名为
AirCondition
的结构体,它有一个
temperature
字段。这个结构体实现了
new
方法来创建一个新的
AirCondition
实例,
adjust_temperature
方法来调整温度,以及
current_temperature
方法来获取当前温度。
17 - 20 行:引入了一些需要的库和模块。
21 - 33 行:定义了一个名为
MyWorld
的结构体,它有一个
air_condition
字段。这个结构体实现了
new
方法来创建一个新的
MyWorld
实例。
MyWorld
结构体上的
#[derive(World)]
属性表示这个结构体将被用作 Cucumber 测试的世界对象,世界对象是在测试中共享的状态。
35 - 49 行:实现了 air.feature 中的用例语句。
用例语句通过宏定义的方式声明。
在 Cucumber 测试框架中,我们通常使用 "Given-When-Then" 的格式来描述测试场景:
- "Given" 步骤用于设置测试的初始条件,通常包括初始化世界对象(World)中的数据。
- "When" 步骤用于触发测试中的事件或行为,这些事件或行为会改变世界对象,从而模拟测试场景。
- "Then" 步骤用于验证世界对象是否按照预期发生了变化。
Rust Cucumber中,有两种匹配用例的方式,分别为:正则表达式和 Cucumber 表达式。Cucumber 表达式相对正则表达式要简单。我们采用Cucumber 表达式匹配用例。
less
复制代码
//Regular expressions #[given(regex = r"^空调当前温度为:\d$")] //Cucumber Expressions #[given(expr = "空调当前温度为:{int}")]
使用 Cucumber 表达式时,需注意表达式中参数的定义。参数定义种类如下:
声明完表达式后,下面自定义一个函数,来实现此条用例。函数名字可以自己任意命名,第一个参数是定义的 World 可变类型,后面跟着表达式中声明的参数。
需要将声明的表达式,提前引入到 cucumber 执行函数的前面。不然 cucumber 找不到这条用例的实现。
50 - 57 行:定义了一个异步的
main
函数,它创建了一个 Cucumber 测试运行器,注册了一个在每个场景开始之前运行的钩子,然后运行位于 "tests/features/air.feature" 的所有特性测试,并在测试完成后退出程序。Cucumber的 run 方法包含多种链式调用方法,可以设置各种运行条件。
Mockall 介绍
mockall
是一个在 Rust 中创建 mock 对象的库。它可以自动为你的代码生成 mock 对象,这对于单元测试非常有用,因为你可以使用这些 mock 对象来模拟复杂的业务逻辑,而不需要实际执行这些逻辑。
mockall
的主要特性包括:
- 自动为 trait 和结构体生成 mock 对象。
- 支持静态和动态方法,以及关联函数和关联常量。
- 支持泛型方法和泛型结构体。
- 支持自定义预期行为和返回值。
- 支持检查方法是否被调用,以及被调用的次数和参数。
使用
mockall
的基本步骤是:
- 使用
#[automock]
属性宏为你的 trait 或结构体生成 mock 对象。 - 在你的测试中,使用生成的 mock 对象替代实际的对象。
- 使用
expect_...
方法设置预期的行为和返回值。 - 使用
assert_called...
方法检查方法是否被正确地调用。
例如:
rust
复制代码
use mockall::automock; #[automock] trait MyTrait { fn my_method(&self, x: u32) -> u32; } #[test] fn test_my_method() { let mut mock = MockMyTrait::new(); mock.expect_my_method() .with(mockall::predicate::eq(5)) .times(1) .returning(|x| x + 1); assert_eq!(mock.my_method(5), 6); mock.checkpoint(); }
在这个例子中,
MockMyTrait
是
MyTrait
的 mock 对象,
expect_my_method
方法设置了
my_method
方法的预期行为和返回值,
checkpoint
方法检查
my_method
方法是否被正确地调用。
实例讲解
通过一个具体例子,来学习单元测试的部署和运行。
文件目录
vbnet
复制代码
. ├── Cargo.toml ├── src │ └── lib.rs └── tests ├── example.rs ├── fake │ ├── air_mock.rs │ └── mod.rs ├── features │ └── air.feature └── steps ├── air_step.rs └── mod.rs
Cargo.toml:工程配置文件
src:业务代码文件夹
src/lib.rs:业务代码
tests:单元测试文件夹
example.rs:cucumber框架启动文件
fake:moke 对象文件夹
air_mock.rs:定义了一个空调 mock struct
features:feature 用例文件夹
steps:用例实现文件夹
air_step.rs:air.feature的用例实现
文件讲解
在 Cargo.toml 中,引入需要的库
ini
复制代码
[package] name = "cucumber_demo" version = "0.1.0" edition = "2021" [dependencies] proc-macro2 = "=1.0.79" [dev-dependencies] cucumber = "0.21.0" tokio = { version = "1.10", features = ["macros", "rt-multi-thread", "time"] } futures = "0.3" mockall = "0.12.1" [[test]] name = "example" # this should be the same as the filename of your test target harness = false
在 lib.rs 中,实现空调 struct
rust
复制代码
pub trait TemperatureTrait: std::fmt::Debug { fn temperature(&self) -> i8; fn adjust_temperature(&self, value: i8); } #[derive(Debug)] pub struct AirCondition { pub temperature_trait: Box<dyn TemperatureTrait>, } impl AirCondition { pub fn new(temp_trait: impl TemperatureTrait + 'static) -> Self { AirCondition { temperature_trait: Box::new(temp_trait), } } }
这里定义了一个 trait:TemperatureTrait,该 trait 有两个方法temperature,adjust_temperature,分别表示获取温度和设置温度。定义了一个 struct:AirCondition,它含有一个实现TemperatureTrait的特征对象。
在 air_mock.rs中,mock 一个实现了TemperatureTrait的结构体:
rust
复制代码
use cucumber_demo::TemperatureTrait; use mockall::{mock, predicate::*}; mock! { #[derive(Debug)] pub Temperature { } impl TemperatureTrait for Temperature { fn temperature(&self) -> i8; fn adjust_temperature(&self,value:i8); } }
在 air.feature 中,声明用例:
yaml
复制代码
Feature: air feature Background: Given 初始化空调 Scenario: 测试空调温度调节1 Given 空调当前温度为:20 When 调整温度为:-2 Then 当前温度应该为:18 Scenario: 测试空调温度调节2 Given 空调当前温度为:21 When 调整温度为:2 Then 当前温度应该为:23 Scenario: 测试空调温度调节3 Given 空调当前温度为:22 When 调整温度为:-3 Then 当前温度应该为:19
在air_step.rs中,实现用例:
rust
复制代码
use crate::fake::air_mock::MockTemperature; use crate::MyWorld; use cucumber::{given, then, when}; use cucumber_demo::AirCondition; use std::sync::RwLock; static mut TEMPERATURE: RwLock<i8> = RwLock::new(0); #[given(expr = "初始化空调")] fn init_air_condition(world: &mut MyWorld) { let mut mock = MockTemperature::new(); mock.expect_temperature() .returning(|| unsafe { *TEMPERATURE.read().unwrap() }); mock.expect_adjust_temperature().returning(|value| unsafe { let mut write = TEMPERATURE.write().unwrap(); *write += value; }); world.air_condition = Some(AirCondition::new(mock)); } #[given(expr = "空调当前温度为:{int}")] fn set_temperature(world: &mut MyWorld, temperature: i8) { unsafe { let mut write = TEMPERATURE.write().unwrap(); *write = temperature; } } #[when(expr = "调整温度为:{int}")] fn adjust_temperature(world: &mut MyWorld, value: i8) { world .air_condition .as_mut() .unwrap() .temperature_trait .adjust_temperature(value); } #[then(expr = "当前温度应该为:{int}")] fn check_temperature(world: &mut MyWorld, expected_temperature: i8) { assert_eq!( world .air_condition .as_mut() .unwrap() .temperature_trait .temperature(), expected_temperature ); }
在example.rs中,启动测试框架:
rust
复制代码
use cucumber::World; use cucumber_demo::AirCondition; use futures::FutureExt; use tokio::time; mod fake; mod steps; #[derive(Debug, World)] #[world(init = Self::new)] struct MyWorld { air_condition: Option<AirCondition>, } impl MyWorld { fn new() -> Self { MyWorld { air_condition: None, } } } #[tokio::main] async fn main() { MyWorld::cucumber() .max_concurrent_scenarios(1) .before(|_feature, _rule, _scenario, _world| { time::sleep(time::Duration::from_millis(1000)).boxed_local() }) .run_and_exit("tests/features") .await }
运行结果如下:
同步运行
细心的读者会发现,cucumber 的执行方法中,多了一行 max_concurrent_scenarios(1)。这是要求 cucumber 在运行时,以同步的方式运行用例。避免并行方式下,结果产生混乱。
还有两种方法,可以让 cucumber 在同步方式下运行。
- 通过命令行的方式运行单元测试,添加 -- --concurrency=1 参数
bash
复制代码
cargo test --test example -- --concurrency=1
- 通过给用例添加@serial标签,表示该用例以同步方式执行
css
复制代码
@serial Scenario: 测试空调温度调节1 Given 空调当前温度为:20 When 调整温度为:-2 Then 当前温度应该为:18
单元测试覆盖率
在工程目录运行命令
css
复制代码
cargo llvm-cov --html
命令执行后,可在 target/llvm-cov/html目录,打开 index.html,查看覆盖率
结语
版权归原作者 糖糖老师436 所有, 如有侵权,请联系我们删除。