#instance #struct #proc-macro #generate #procedural #static #testing

bin tlayuda

为结构体添加生成结构体实例的静态方法的过程宏,配置简单

3个版本

0.1.6 2021年3月28日
0.1.5 2021年3月26日
0.1.4 2021年3月25日

过程宏中排名第535位

MIT许可证

43KB
287

githubcrates-iodocs-rs

Tlayuda - 测试对象构建器

为结构体添加生成结构体实例的静态方法的过程宏,配置简单。目标是提供一个简单的方法来生成动态测试数据,同时只需要设置与给定单元测试相关的对象字段。

用法示例

#[derive(Tlayuda)]
pub struct Student {
    id: u64,
    first_name: String,
    last_name: String,
    telephone: String,
    date_of_birth: String,
    final_grade: u32,
}

fn group_students_by_grade(mut students: Vec<Student>) -> StudentsPartitionedByGrade {
    let result = StudentsPartitionedByGrade {
        a_students: Vec::new(),
        b_students: Vec::new(),
        c_students: Vec::new(),
        d_students: Vec::new(),
        f_students: Vec::new(),
    };

    students.drain(..).fold(result, |mut acc, student| {
        match student.final_grade {
            90..=100 => acc.a_students.push(student),
            80..=89 => acc.b_students.push(student),
            70..=79 => acc.c_students.push(student),
            60..=69 => acc.d_students.push(student),
            0..=50 => acc.f_students.push(student),
            _ => (),
        }

        acc
    })
}

给定上述代码,覆盖 group_students_by_grade 所有方面的单元测试需要手动构建多个Student实例,并设置函数测试中未使用的字段。Tlayuda帮助填充假数据,并提供仅设置您关心的字段的函数。

#[test]
fn test_group_students_by_grade() {
    // sets up a vec of students with 10 students per number grade
    let students = Student::tlayuda()                  // create tlayuda test builder
        .set_final_grade(|index| (index % 101) as u32) // returns a 0-100 grade based on index
        .build_vec(200);                               // creates a vec of 200 students

    // call function we're testing
    let result = group_students_by_grade(students);

    // verifies expected # of students per group
    assert_eq!(20, result.a_students.len());
    assert_eq!(20, result.b_students.len());
    assert_eq!(20, result.c_students.len());
    assert_eq!(20, result.d_students.len());
    assert_eq!(102, result.f_students.len());

    // verifies every group has the correct grade range
    result.a_students.iter().for_each(|s| assert!(s.final_grade >= 90 && s.final_grade <= 100));
    result.b_students.iter().for_each(|s| assert!(s.final_grade >= 80 && s.final_grade < 90));
    result.c_students.iter().for_each(|s| assert!(s.final_grade >= 70 && s.final_grade < 80));
    result.d_students.iter().for_each(|s| assert!(s.final_grade >= 60 && s.final_grade < 70));
    result.f_students.iter().for_each(|s| assert!(s.final_grade <= 50));
}

完整代码示例在这里

如何使用

在结构体上方添加Tlayuda过程宏。

use tlayuda::*;

#[derive(Tlayuda)]
pub struct Person {
    id: u32,
    first_name: String,
    last_name: String,
    is_active: bool
}

这将根据源结构的名称生成一个构建器结构体Tlayuda{}Builder。在源结构体tlayuda上添加一个静态方法,该方法返回构建器结构体的一个实例。在构建器对象上调用build()将根据每个字段的类型和其索引(构建器存储的递增ID)使用“动态默认值”生成源结构体实例。

    let mut builder = Person::tlayuda();
    let person = builder.build();

    assert_eq!(0, person.id);
    assert_eq!("first_name0", person.first_name);
    assert_eq!("last_name0", person.last_name);
    assert_eq!(false, person.is_active);

    // builder increments the internal index 
    // with each call to build
    let person = builder.build();

    assert_eq!(1, person.id);
    assert_eq!("first_name1", person.first_name);
    assert_eq!("last_name1", person.last_name);
    assert_eq!(false, person.is_active);

构建器还将为结构体中的每个字段提供一个以set_为前缀的方法,该方法接受一个闭包,其形式为FnMut(usize) -> Typeusize参数是构建对象的“索引”。闭包的返回类型应与字段的类型相匹配。这可以用来设置自定义的字段值,而无需设置与当前测试无关的整个结构体。

    let mut builder = Person::tlayuda()
        .set_first_name(|i| {
            if i == 1 { 
                "Michael".to_string()
            } else {
                format!("first_name{}", i)
            }
        });
        
    let person = builder.build();
    assert_eq!("first_name0", person.first_name);

    let person = builder.build();
    assert_eq!("Michael", person.first_name);

构建器还可以通过调用build_vec生成结构体的Vec::<_>。这内部使用构建器的当前设置来生成数据,并在每次构建后递增索引。

    // builds 1000 Person objects and verifies
    // each object has a unique first_name value
    Person::tlayuda()
        .set_first_name(|i| i.to_string())
        .build_vec(1000)
        .iter()
        .enumerate()
        .for_each(|(i, x)| {
            assert_eq!(i.to_string(), x.first_name);
        });

您还可以通过调用with_index(index: usize)来更改构建器的起始索引。此外,对构建器对象的每次调用(不包括对buildbuild_vec的调用)都会返回构建器,以允许链式调用。

    Person::tlayuda()
        .set_first_name(|i| match (i % 3, i % 5) {
            (0, 0) => "FizzBuzz".into(),
            (0, _) => "Fizz".into(),
            (_, 0) => "Buzz".into(),
            _ => i.to_string(),
        })
        .with_index(1)
        .build_vec(100)

Tlayuda还会自动尝试递归构建字段,如果它们不是已知支持的类型之一。也就是说,如果struct A有一个字段是struct B(它也使用了Tlayuda推导宏),则struct A构建器将自动调用struct B的构建器。注意:如果内部结构体有不受支持的字段或未使用Tlayuda宏,这会导致编译错误。

    #[derive(Tlayuda)]
    pub struct StructA {
        pub some_field: u32,
        pub another_struct: StructB,
    }

    #[derive(Tlayuda)]
    pub struct StructB {
        pub field_on_b: String,
    }

    let some_A = StructA::tlayuda().build();
    assert_eq!("field_on_b0", some_A.another_struct.field_on_struct_b);

支持的类型

目前Tlayuda支持仅由以下类型组成的结构体

  • 数字原始类型(i8-i128,u8-u128,f32,f64,isize,usize)
  • 布尔值
  • 字符
  • 字符串、OsString
  • Vecs
  • 具有数字原始类型的数组
  • 仅由上述类型组成(且使用Tlayuda宏)的结构体

具有完整路径的类型将忽略其路径,并按最后一段的处理方式表现。例如,“std::ffi::OsString”将视为“OsString”。

虽然目标是支持尽可能多的类型,但当前可能会遇到不支持的类型。在不受支持的字段上方添加一个 tlayuda_ignore 属性将标记该字段为跳过。相反,将修改 .tlayuda() 函数以接受该类型的参数,该参数将在构建过程中克隆以填充该字段。

    #[derive(Tlayuda)]
    pub strut StructA {
        pub some_field: u32,
        pub some_other_field: bool,
        #[tlayuda_ignore] // add attribute above unsupported types
        pub some_unsupported_type: Vec::<u32>,
    }

    /* inside a test */

    let some_vec: Vec::<u32> = vec![1, 2, 3]; // construct a value for the unsupported type
    let mut builder = StructA::tlayuda(some_vec); // ignored field now required as a parameter instead of being handled by tlayuda
    let some_1 = builder.build(); 

    assert_eq!(100, some_1.some_unsupported_type[0]); // value gets populated with value passed into tlayuda()

    let some_2 = builder.build(); 
    assert_eq!(100, some_2.some_unsupported_type[0]); // value is cloned across builds

在测试之外运行

默认情况下,Tlayuda 仅在执行测试时工作;宏使用 cfg[(test)] 属性输出代码,因此它仅影响测试。虽然对象的结构应在 Tlayuda 的各个版本之间保持一致,但生成的代码的意图和设计是针对测试目的。如果您需要在测试之外使用 Tlayuda,可以通过启用 "allow_outside_tests" 功能来实现。

当前待办事项列表

  • 添加 vec 作为支持类型
  • 修复失败的 Doc 测试
  • 向 tlayuda_ignore 属性添加 "order" 参数以自定义 tlayuda() 参数顺序
  • 为数组添加更多类型支持(包括嵌套数组)
  • 支持 HashMaps
  • 支持由当前支持的类型组成的元组
  • 添加匹配访问修饰符(public/private)以避免泄露私有类型

依赖项

~1.5MB
~34K SLoC