2 个版本

0.0.2 2024年1月18日
0.0.1 2024年1月15日

#145 in 测试

MIT/Apache

485KB
9K SLoC

clearcheck

clearcheck

目录

介绍 Clearcheck

轻松编写表达性和优雅的断言!

clearcheck 旨在使断言语句尽可能清晰和简洁。它允许链式调用多个断言,以实现流畅和直观的语法,从而生成更多自文档化的测试用例。

let pass_phrase = "P@@sw0rd1 zebra alpha";
pass_phrase.should_not_be_empty()
    .should_have_at_least_length(10)
    .should_contain_all_characters(vec!['@', ' '])
    .should_contain_a_digit()
    .should_not_contain_ignoring_case("pass")
    .should_not_contain_ignoring_case("word");

主要功能

🔹 流畅的 API:链式调用断言以获得自然和可读的体验。

🔹 广泛的断言:涵盖常见验证需求的多种断言。

🔹 可定制:根据特定领域需求扩展自己的断言。

🔹 类型安全:利用 Rust 的类型系统构建,确保断言可靠且具有表现力。

🔹 自定义断言:根据精确需求定制断言,确保对各种数据结构进行全面的验证。

使用方法

将以下内容添加到您的 Cargo.toml 中(无特性)

[dev-dependencies]
clearcheck = { version = "0.0.2" }

将以下内容添加到您的 Cargo.toml 中(所有特性)

[dev-dependencies]
clearcheck = { version = "0.0.2", features = ["num", "date", "regex", "file"] }
chrono = { version = "0.4.31" }
num = { version = "0.4.1" }
regex = { version = "1.10.2" }
walkdir = { version = "2.4.0", features = [] }

断言与匹配器

断言是测试用例的基石,定义了代码必须满足的确切期望。它们充当合同,确保每个数据类型(/数据结构)都遵循其预期行为。

另一方面,匹配器提供了执行这些断言的细粒度工具。它们检查数据并验证数据是否符合特定标准。

本质上,断言协调高级验证逻辑,而匹配器作为代码级别的检查器,确保每个细节都与预期相符。

支持的断言

布尔类型断言

断言
断言 描述
should_be_true 断言布尔值评估为 true。
should_be_false 断言布尔值评估为 false。
使用方法
let value = true;
value.should_be_true();

字符类型断言

断言
断言 描述
should_be_in_inclusive_range 断言字符位于给定的包含范围内。
should_not_be_in_inclusive_range 断言字符不在给定的包含范围内。
should_be_in_exclusive_range 断言字符在给定的排除范围内。
should_not_be_in_exclusive_range 断言字符不在给定的排除范围内。
should_be_equal_ignoring_case 断言字符等于其他字符,忽略大小写。
should_not_be_equal_ignoring_case 断言字符不等于其他字符,忽略大小写。
使用方法
let letter = 'd';
letter.should_be_in_inclusive_range('a'..='d');

let letter = 'D';
letter.should_be_equal_ignoring_case('d');

集合(Vector、数组、切片)

断言
断言 描述
should_have_upper_bound 断言集合中的所有元素都小于或等于给定元素。
should_have_lower_bound 断言集合中的所有元素都大于或等于给定元素。
should_contain_duplicates 断言集合中至少有一个重复元素。
should_not_contain_duplicates 断言集合中不包含任何重复元素。
should_be_equal_ignoring_case 断言集合中的元素等于其他集合中的元素,忽略大小写差异。(仅适用于元素可以表示为字符串的情况
should_not_be_equal_ignoring_case 断言集合中的元素不等于其他集合中的元素,忽略大小写差异。(仅适用于元素可以表示为字符串的情况
should_be_monotonically_increasing 断言集合中的元素是非递减顺序(允许连续相等的元素)。
should_be_monotonically_decreasing 断言集合中的元素是非递增顺序(允许连续相等的元素)。
should_be_strictly_increasing 断言集合中的元素是严格递增顺序(不允许连续元素相等)。
should_be_strictly_decreasing 断言集合中的元素是严格递减顺序(不允许连续元素相等)。
should_contain 断言集合包含给定元素。
should_not_contain 断言集合不包含给定元素。
should_contain_all 断言集合包含所有给定元素。
should_not_contain_all 断言集合不包含所有给定元素。
should_contain_any 断言集合包含给定元素中的任何一个。
should_not_contain_any 断言集合不包含给定元素中的任何一个。
should_be_empty 断言集合为空。
should_not_be_empty 断言集合不为空。
基于大小的断言
断言 描述
should_have_size 断言底层集合的大小正好是给定的大小。
should_not_have_size 断言底层集合的大小不是给定的大小。
should_have_at_least_size 断言底层集合的大小大于或等于给定的大小。
should_have_at_most_size 断言底层集合的大小小于或等于给定的大小。
should_be_same_size_as 断言底层集合的大小与给定集合的大小相同。
should_have_size_in_inclusive_range 断言底层集合的大小在给定的包含范围内。
should_not_have_size_in_inclusive_range 断言底层集合的大小不在给定的包含范围内。
should_have_size_in_exclusive_range 断言底层集合的大小在给定的排除范围内。
should_not_have_size_in_exclusive_range 断言底层集合的大小不在给定的排除范围内。
使用方法
let keywords = ["testing", "automation", "clearcheck", "junit"];
keywords.should_not_be_empty()
    .should_have_size_in_inclusive_range(4..=10)
    .should_not_contain_duplicates()
    .should_contain_any(vec!["junit", "clearcheck", "testing"])
    .should_not_contain_any(vec!["scalatest", "gotest"]);

日期(由 'date' 功能启用,依赖于 chrono

断言
断言 描述
should_have_same_year_as 断言日期与另一个日期具有相同的年份。
should_not_have_same_year_as 断言日期与其他日期不具有相同的年份。
should_have_year 断言日期与给定年份具有相同的年份。
should_not_have_year 断言日期与给定年份不具有相同的年份。
should_have_same_month_as 断言日期与其他日期具有相同的月份。
should_not_have_same_month_as 断言日期与其他日期不具有相同的月份。
should_have_month 断言日期与给定月份具有相同的月份。
should_not_have_month 断言日期与给定月份不具有相同的月份。
should_have_same_day_as 断言日期与其他日期具有相同的日期。
should_not_have_same_day_as 断言日期与其他日期不具有相同的日期。
should_have_day 断言日期与给定日期具有相同的日期。
should_not_have_day 断言日期与给定日期不具有相同的日期。
should_be_a_leap_year 断言日期在闰年。
should_not_be_a_leap_year 断言日期不在闰年。
使用方法
use chrono::NaiveDate;

let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
date
     .should_be_a_leap_year()
     .should_have_month(1)
     .should_be_greater_than(&NaiveDate::from_ymd_opt(2023, 1, 10).unwrap());

文件路径(由 'file' 功能启用,依赖于 walkdir

断言
断言 描述
should_be_a_directory 断言路径是目录。
should_be_a_file 断言路径是文件。
should_be_a_symbolic_link 断言路径是符号链接。
should_be_zero_sized 断言路径对应于零大小文件。
should_not_be_zero_sized 断言路径对应于非零大小文件。
should_be_readonly 断言路径对应于只读文件。
should_be_writable 断言路径对应于可写文件。
should_be_absolute 断言路径是绝对路径。
should_be_relative 断言路径是相对路径。
should_have_extension 断言路径对应于具有给定扩展名的文件。
should_not_have_extension 断言路径对应于没有给定扩展名的文件。
should_contain_file_name 断言路径对应于包含给定文件名的目录。
should_not_contain_file_name 断言路径对应于不包含给定文件名的目录。
should_contain_all_file_names 断言路径对应于包含所有给定文件名的目录。
should_not_contain_all_file_names 断言路径对应于不包含所有给定文件名的目录。
should_contain_any_of_file_names 断言路径对应于包含任何给定文件名的目录。
should_not_contain_any_of_file_names 断言路径对应于不包含任何给定文件名的目录。
使用方法
use tempdir::TempDir;

let temporary_directory = TempDir::new(".").unwrap();
let file_path_junit = temporary_directory.path().join("junit.txt");
let file_path_clearcheck = temporary_directory.path().join("clearcheck.txt");

let _ = File::create(file_path_junit).unwrap();
let _ = File::create(file_path_clearcheck).unwrap();

let directory_path = temporary_directory.path();
directory_path
    .should_be_a_directory()
    .should_contain_any_of_file_names(vec!["junit.txt", "clearcheck.txt"]);

浮点数(由 'num' 功能启用,依赖于 num

断言
断言 描述
should_be_nan 断言浮点值是 NaN(不是一个数)。
should_not_be_nan 断言浮点值不是 NaN(不是一个数)。
should_be_zero 断言浮点值是零。
should_not_be_zero 断言浮点值不是零。
should_be_positive 断言浮点值是正数。
should_be_negative 断言浮点值是负数。
should_be_in_inclusive_range_with_tolerance 断言浮点值在给定包含范围内,并具有容差。
should_not_be_in_inclusive_range_with_tolerance 断言浮点值不在给定包含范围内,并具有容差。
should_be_in_exclusive_range_with_tolerance 断言浮点值在给定排除范围内,并具有容差。
should_not_be_in_exclusive_range_with_tolerance 断言浮点值不在给定的容忍度范围内的排他区间内。
使用方法
let value: f64 = 1.34589;
value
   .should_not_be_nan()
   .should_be_positive()
   .should_be_in_inclusive_range_with_tolerance(1.11..=1.3458, 0.23);

整数(由'num'功能启用,取决于num

断言
断言 描述
should_be_positive 断言整数值为正。
should_be_negative 断言整数值为负。
should_be_even 断言整数值为偶数。
should_be_odd 断言整数值为奇数。
should_be_zero 断言整数值为零。
should_not_be_zero 断言整数值不为零。
使用方法
let value = 24;
value
    .should_be_positive()
    .should_be_even()
    .should_be_in_inclusive_range(10..=40);

HashMap 类型断言

断言
断言 描述
should_contain_key 断言HashMap包含给定的键。
should_not_contain_key 断言HashMap不包含给定的键。
should_contain_all_keys 断言HashMap包含所有给定的键。
should_not_contain_all_keys 断言HashMap不包含所有给定的键。
should_contain_any_of_keys 断言HashMap包含给定的任一键。
should_not_contain_any_of_keys 断言HashMap不包含给定的任一键。
should_contain_value 断言HashMap包含给定的值。
should_not_contain_value 断言HashMap不包含给定的值。
should_contain_all_values 断言HashMap包含所有给定的值。
should_not_contain_all_values 断言HashMap不包含所有给定的值。
should_contain_any_of_values 断言HashMap包含给定的任一值。
should_not_contain_any_of_values 断言HashMap不包含给定的任一值。
should_contain 断言HashMap包含给定的键和值。
should_not_contain 断言HashMap不包含给定的键和值。
should_contain_all 断言HashMap包含给定HashMap的所有条目。
should_not_contain_all 断言HashMap不包含给定HashMap的所有条目。
should_contain_any 断言HashMap包含给定HashMap的任一条目。
should_not_contain_any 断言HashMap不包含给定HashMap的任一条目。
should_be_empty 断言HashMap为空。
should_not_be_empty 断言HashMap不为空。
+ 基于大小的断言.
使用方法
#[derive(Eq, Debug, PartialEq, Hash)]
struct Book {
    id: usize,
    title: &'static str,
}

impl Book {
    fn new(id: usize, title: &'static str) -> Self {
        Book { id, title }
    }
}

let mut book_id_by_name = HashMap::new();
book_id_by_name.insert("Database internals", 1);
book_id_by_name.insert("Designing data intensive applications", 2);

book_id_by_name
    .should_not_be_empty()
    .should_contain_key("Database internals")
    .should_contain_value(&1)
    .should_have_at_least_size(2)
    .should_contain("Database internals", &1);

Option 类型断言

断言
断言 描述
should_be_some 断言Option评估为Some。
should_be_none 断言Option评估为None。
使用方法
let option = Some("clearcheck");
option.should_be_some();

Result 类型断言

断言
断言 描述
should_be_ok 断言Result评估为Ok。
should_be_err Result评估为Err。
使用方法
let value: Result<i32, &str> = Ok(32);
value.should_be_ok();

T: PartialOrd

断言
断言 描述
should_be_greater_than 断言self值根据PartialOrd实现大于给定的值(other)。
should_be_greater_than_equal_to 断言self值根据PartialOrd实现大于或等于给定的值(other)。
should_be_less_than 断言self值根据PartialOrd实现小于给定的值(other)。
should_be_less_than_equal_to 断言self值根据PartialOrd实现小于或等于给定的值(other)。
should_not_be_greater_than 断言self值根据PartialOrd实现不大于给定的值(other)。
should_not_be_greater_than_equal_to 断言self值根据PartialOrd实现不大于或等于给定的值(other)。
should_not_be_less_than 断言self值根据PartialOrd实现不小于给定的值(other)。
should_not_be_less_than_equal_to 断言self值根据PartialOrd实现不小于或等于给定的值(other)。
should_be_in_inclusive_range 断言self值在给定的包含范围内。
should_not_be_in_inclusive_range 断言self值不在给定的包含范围内。
should_be_in_exclusive_range 断言self值在给定的排他范围内。
should_not_be_in_exclusive_range 断言self的值不在给定的排他性范围内。
使用方法
let value = 12.56;
value
    .should_be_greater_than(&10.90)
    .should_be_less_than(&15.98)
    .should_be_in_inclusive_range(10.90..=13.10);

T: Eq

断言
断言 描述
should_equal 断言self持有的值等于other。
should_not_equal 断言self持有的值不等于other。
使用方法
#[derive(Debug, Eq, PartialEq)]
struct Book { name: &'static str }

let books = vec![
  Book {name: "Database internals"},
  Book {name: "Rust in action"}
];

let other = vec![
  Book {name: "Database internals"},
  Book {name: "Rust in action"}
];

books.should_equal(&other);

字符串类型断言

断言
断言 描述
should_begin_with 断言字符串以给定的前缀开头。
should_not_begin_with 断言字符串不以给定的前缀开头。
should_end_with 断言字符串以给定的后缀结尾。
should_not_end_with 断言字符串不以给定的后缀结尾。
should_be_lower_case 断言字符串是小写。
should_be_upper_case 断言字符串是大写。
should_be_equal_ignoring_case 断言字符串等于其他字符串,忽略大小写。
should_not_be_equal_ignoring_case 断言字符串不等于其他字符串,忽略大小写。
should_only_contain_digits 断言字符串只包含数字。
should_contain_a_digit 断言字符串包含一个数字。
should_not_contain_digits 断言字符串不包含任何数字。
should_contain_character 断言字符串包含给定的字符。
should_not_contain_character 断言字符串不包含给定的字符。
should_contain_all_characters 断言字符串包含所有给定的字符。
should_not_contain_all_characters 断言字符串不包含所有给定的字符。
should_contain_any_characters 断言字符串包含给定的任何字符。
should_not_contain_any_characters 断言字符串不包含给定的任何字符。
should_contain 断言字符串包含给定的子字符串。
should_not_contain 断言字符串不包含给定的子字符串。
should_contain_ignoring_case 断言字符串包含子字符串,忽略大小写差异。
should_not_contain_ignoring_case 断言字符串不包含子字符串,忽略大小写差异。
should_be_empty 断言字符串为空(零字符)。
should_not_be_empty 断言字符串不为空。
should_be_numeric 断言字符串是数值的。
should_not_be_numeric 断言字符串不是数值的。
should_match 断言字符串与给定的正则表达式匹配。(由 'regex' 功能启用,取决于regex
should_not_match 断言字符串不与给定的正则表达式匹配。(由 'regex' 功能启用,取决于regex
基于长度的断言
断言 描述
should_have_length 断言字符串的长度正好是给定的长度。
should_not_have_length 断言字符串的长度不是给定的长度。
should_have_at_least_length 断言字符串的长度大于或等于给定的长度。
should_have_at_most_length 断言字符串的长度小于或等于给定的长度。
should_have_length_in_inclusive_range 断言字符串的长度在给定的包含范围内。
should_not_have_length_in_inclusive_range 断言字符串的长度不在给定的包含范围内。
should_have_length_in_exclusive_range 断言字符串的长度在给定的排他性范围内。
should_not_have_length_in_exclusive_range 断言字符串的长度不在给定的排他性范围内。
使用方法
let pass_phrase = "P@@sw0rd1 zebra alpha";
pass_phrase.should_not_be_empty()
    .should_have_at_least_length(10)
    .should_contain_all_characters(vec!['@', ' '])
    .should_contain_a_digit()
    .should_not_contain_ignoring_case("pass")
    .should_not_contain_ignoring_case("word");

变更日志

版本 0.0.2

版本0.0.2重构了字符串断言以消除重复,并引入了以下断言

集合(基于谓词的断言)
断言 描述
should_satisfy_for_all 断言集合中所有元素都满足给定的谓词。
should_not_satisfy_for_all 断言集合中并非所有元素都满足给定的谓词。
should_satisfy_for_any 断言集合中的任何元素都满足给定的谓词。
should_not_satisfy_for_any 断言集合中没有任何元素满足给定的谓词。
集合(基于最小-最大值的断言)
断言 描述
should_have_min 断言底层集合中的最小值等于给定的最小值。
should_not_have_min 断言底层集合中的最小值不等于给定的最小值。
should_have_max 断言底层集合中的最大值等于给定的最大值。
should_not_have_max 断言底层集合中的最大值不等于给定的最大值。
should_have_min_in_inclusive_range 断言底层集合中的最小值在给定的包含范围内。
should_not_have_min_in_inclusive_range 断言底层集合中的最小值不在给定的包含范围内。
should_have_min_in_exclusive_range 断言底层集合中的最小值在给定的不包含范围内。
should_not_have_min_in_exclusive_range 断言底层集合中的最小值不在给定的不包含范围内。
should_have_max_in_inclusive_range 断言底层集合中的最大值在给定的包含范围内。
should_not_have_max_in_inclusive_range 断言底层集合中的最大值不在给定的包含范围内。
should_have_max_in_exclusive_range 断言底层集合中的最大值在给定的不包含范围内。
should_not_have_max_in_exclusive_range 断言底层集合中的最大值不在给定的不包含范围内。
选项(基于谓词的断言)
断言 描述
should_be_some_and_satisfy 断言Option值是Some并且满足给定的谓词。
should_be_some_and_not_satisfy 断言Option值是Some但不满足给定的谓词。
结果(基于谓词的断言)
断言 描述
should_be_ok_and_satisfy 断言Result值是Ok并且满足给定的谓词。
should_be_ok_and_not_satisfy 断言Result值是Ok但不满足给定的谓词。

释放自定义匹配器和断言的威力

虽然这个crate(软件包)包含了许多现成的断言,但有时您的测试需求可能需要定制的解决方案。"clearcheck"允许您创建自己的自定义匹配器和断言!

可能性是无限的

  • 领域特定验证:创建理解您业务逻辑细微差别的断言。
  • 增强可读性:编写清晰简洁的匹配器,使其反映领域词汇,使您的测试具有自文档性和可理解性。
  • 减少冗余:通过封装在可重用匹配器中的复杂验证逻辑来消除重复代码。

让我们创建一个具有特定标准(如长度、数字和禁止模式)的自定义密码匹配器。

fn be_a_valid_password<T: AsRef<str> + Debug>() -> Matchers<T> {
    MatchersBuilder::start_building_with_inverted(be_empty().boxed())
      .push(have_atleast_same_length(10).boxed())
      .push(contain_a_digit().boxed())
      .push(contain_any_of_characters(vec!['@', '#']).boxed())
      .push_inverted(begin_with("pass").boxed())
      .push_inverted(contain_ignoring_case("pass").boxed())
      .push_inverted(contain_ignoring_case("word").boxed())
      .combine_as_and()
}

匹配器强制以下条件

  • 输入不能为空。
  • 输入必须至少有10个字符长度。
  • 输入必须至少包含一个数字。
  • 输入必须包含以下字符之一:'@','#'。
  • 输入不能以字符串"pass"开头(不区分大小写)。
  • 输入不能包含字符串"pass"或"word"(不区分大小写)。

让我们将其组合成一个用于有效密码的强大断言。

trait PasswordAssertion {
    fn should_be_a_valid_password(&self) -> &Self;
}

impl PasswordAssertion for &str {
    fn should_be_a_valid_password(&self) -> &Self {
        self.should(&be_a_valid_password());
        self
    }
}

现在是时候使用我们的密码断言了。

#[test]
fn should_be_a_valid_password() {
    let password = "P@@sw0rd9082";
    password.should_be_a_valid_password();
}

让我们为密码的有效性添加一个额外的条件。它必须大于字符串"pass"。这意味着,一个密码有效如果

  • 它满足之前的匹配器条件
  • 它大于字符串"pass"

看起来我们可以在现有的be_a_valid_password方法中组合一个OrderedMatcher

匹配器兼容性背后的关键思想是,只有当匹配器可以处理相同的数据类型时,它们才能组合。

在这种情况下,OrderedMatcher可以处理任何部分有序类型(T: PartialOrd),而be_a_valid_password中的其他匹配器与字符串切片(&str)一起工作。

为了解决这个问题,可以创建一个单独的匹配器be_greater_than_pass来处理“大于'pass'”条件。此匹配器与字符串值一起工作,确保兼容性。

fn be_greater_than_pass() -> Matchers<String> {
    MatchersBuilder::start_building(be_greater_than(String::from("pass")).boxed())
        .push_inverted(be_empty().boxed())
        .combine_as_and()
}

它返回一个Matchers<String>实例,因此现在为String实现了PasswordAssertion。现在,方法should_be_a_valid_password使用组合的匹配器来断言密码是否有效。

impl PasswordAssertion for String {
    fn should_be_a_valid_password(&self) -> &Self {
        self.should(&be_a_valid_password());
        self.should(&be_greater_than_pass());
        self
    }
}

#[test]
fn should_be_a_valid_password() {
    let password = "P@@sw0rd9082".to_string();
    password.should_be_a_valid_password();
}

Rust 特性

clearcheck crate支持以下功能

示例项目

访问示例项目了解此crate的用法。

参考

提及

依赖项

~0–8MB
~52K SLoC