0


rust 单元测试最佳实践

  1. 框架介绍

为了复用原有 Android 端和 iOS 端的 feature 用例,rust 下也选择 cucumber 框架做为单元测试框架。这一选择允许我们无缝地将已有的测试用例迁移至Rust环境中执行。

Cucumber是一种行为驱动开发(BDD)的工具,允许开发人员利用自然语言描述功能测试用例。这些测试用例按特性(Features)组织,并通过一系列场景(Scenarios)详述,每个场景由数个操作步骤(Steps)构成。

rust 下的 cucumber 框架为cucumber,该库为异步测试提供了良好支持。

为了实现接口的模拟测试,推荐使用mockall库,它是Rust中的一个模拟框架,类似于Android的Mockito 以及PowerMock,和iOS的OCMock,支持广泛的模拟功能。

结合Cucumber和mockall,我们能够高效地编写Rust库的单元测试。

  1. Rust Cucumber 介绍

  2. 配置 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

文件,并在里面定义测试的入口点。

  1. 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 方法包含多种链式调用方法,可以设置各种运行条件。

  1. Mockall 介绍

mockall

是一个在 Rust 中创建 mock 对象的库。它可以自动为你的代码生成 mock 对象,这对于单元测试非常有用,因为你可以使用这些 mock 对象来模拟复杂的业务逻辑,而不需要实际执行这些逻辑。

mockall

的主要特性包括:

  • 自动为 trait 和结构体生成 mock 对象。
  • 支持静态和动态方法,以及关联函数和关联常量。
  • 支持泛型方法和泛型结构体。
  • 支持自定义预期行为和返回值。
  • 支持检查方法是否被调用,以及被调用的次数和参数。

使用

mockall

的基本步骤是:

  1. 使用 #[automock] 属性宏为你的 trait 或结构体生成 mock 对象。
  2. 在你的测试中,使用生成的 mock 对象替代实际的对象。
  3. 使用 expect_... 方法设置预期的行为和返回值。
  4. 使用 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

方法是否被正确地调用。

  1. 实例讲解

通过一个具体例子,来学习单元测试的部署和运行。

  1. 文件目录

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的用例实现

  1. 文件讲解

在 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 } 

运行结果如下:

  1. 同步运行

细心的读者会发现,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 
  1. 单元测试覆盖率

在工程目录运行命令

css

复制代码

cargo llvm-cov --html 

命令执行后,可在 target/llvm-cov/html目录,打开 index.html,查看覆盖率

结语

原文链接:https://juejin.cn/post/7376620206339702818

标签: 前端 前端框架

本文转载自: https://blog.csdn.net/2402_85402030/article/details/139494163
版权归原作者 糖糖老师436 所有, 如有侵权,请联系我们删除。

“rust 单元测试最佳实践”的评论:

还没有评论