【问题标题】:How to check if a function has been called in Rust?如何检查是否在 Rust 中调用了函数?
【发布时间】:2019-02-07 21:41:44
【问题描述】:

我有一个功能如下

pub fn registration(student_id: &T::StudentId, registrar: &T::RegistrarID) {
    // More code here.
    if num_of_students < student_limit {
        Self::function_one(&registrar, &num_of_students);
    } else {
        Self::function_two(&num_of_students);
    }
}

在单元测试中,我打算检查是否调用了function_onefunction_two

#[test]
fn registration_more_students_should_call_functon_one() {
    with_test_data(
        &mut TestBuilder::default().num_of_students(1000).build(),
        || {
            //assert_called!(module_name::registration("TV:009", "DF-000-09"));
        },
    );
}

如何测试函数是否在 Rust 中被调用?

【问题讨论】:

  • 我猜你的函数有副作用,你可以检查一下这个效果。
  • 我完全不知道它是什么或它做了什么,但项目名称看起来有点前途:mock_derive

标签: unit-testing rust


【解决方案1】:

强烈的意见警告:你做错了测试。这与“我如何测试私有方法”处于同一级别。您不应该关心registration 的实现到这种详细程度。

话虽如此,如果知道采用哪个if 分支确实很重要,那么使用依赖注入:

fn registration(mut registration: impl Registration, registrar: i32) {
    let num_of_students = 0;
    let student_limit = 0;

    if num_of_students < student_limit {
        registration.function_one(registrar, num_of_students);
    } else {
        registration.function_two(num_of_students);
    }
}

trait Registration {
    fn function_one(&mut self, registrar: i32, num_of_students: i32);
    fn function_two(&mut self, num_of_students: i32);
}

impl<R: Registration> Registration for &'_ mut R {
    fn function_one(&mut self, registrar: i32, num_of_students: i32) {
        (**self).function_one(registrar, num_of_students)
    }
    fn function_two(&mut self, num_of_students: i32) {
        (**self).function_two(num_of_students)
    }
}

/*
// An example implementation for production
struct DatabaseRegistration;

impl Registration for DatabaseRegistration {
    fn function_one(&mut self, registrar: i32, num_of_students: i32) {
        eprintln!("Do DB work: {}, {}", registrar, num_of_students)
    }
    fn function_two(&mut self, num_of_students: i32) {
        eprintln!("Do DB work: {}", num_of_students)
    }
}
*/

#[cfg(test)]
mod test {
    use super::*;

    #[derive(Debug, Copy, Clone, Default)]
    struct TestRegistration {
        calls_to_one: usize,
        calls_to_two: usize,
    }

    impl Registration for TestRegistration {
        fn function_one(&mut self, _: i32, _: i32) {
            self.calls_to_one += 1;
        }
        fn function_two(&mut self, _: i32) {
            self.calls_to_two += 1;
        }
    }

    #[test]
    fn calls_the_right_one() {
        let mut reg = TestRegistration::default();
        registration(&mut reg, 42);
        assert_eq!(1, reg.calls_to_two)
    }
}

完成此操作后,您可以看到registration 调用了适当的特征函数(如示例测试所示)。

这可以防止您的生产代码中散落着特定于测试的碎屑,同时还使您能够更加灵活并快速测试更多用例。

另见:

【讨论】:

  • 我猜你的意见取决于你最熟悉的语言,单元测试应该测试功能而不是其他函数,这里的目的是测试是否从方法中调用了其他函数
  • @Kalanamith 由​​于您不只是简单地测试函数的外部行为(比较返回结果),而且还想查看是否调用了某个其他方法,因此您实际上是在测试副作用(因为方法调用本身就是一种“副作用”,尽管它很奇怪)。在测试中处理任何类型的副作用时,通常的做法是使函数在执行副作用的解释器上泛型,然后注入一个特殊的测试期间的测试解释器。
  • @Kalanamith 从这个意义上说,Shepmaster 在这里提出的建议更接近于我在编写 Scala 代码时通常会做的事情(这种方法似乎越来越与语言无关,并且适用于许多不同的生态系统)。我尝试cfg-方法主要是因为我很好奇它是否会编译,而不是因为我建议将其作为最佳实践。
  • 这个位impl&lt;R: Registration&gt; Registration for &amp;'_ mut R 看起来有点像黑魔法。你能补充一点为什么需要它以及它的作用吗?
  • @AndreyTyukin 你是对的,它会简化这个例子,但我想说这主要是因为只有测试代码可以看。在一个更大的示例中,您希望优先考虑生产代码。也许您希望 R 由包含方法 registration 的结构拥有,以便可以从创建 R 的函数中返回它。见半相关API guideline about taking Read / Write by value
【解决方案2】:

这是在多个地方使用#[cfg(test)] 的幼稚尝试:

struct Registration {
    students: Vec<String>,
    #[cfg(test)]
    function_1_called: bool,
}

impl Registration {
    fn new() -> Self {
        Registration {
            students: Vec::new(),
            #[cfg(test)]
            function_1_called: false,
        }
    }

    fn function_1(&mut self, students: Vec<String>) {
        self.students.extend(students);
        #[cfg(test)]
        {
            self.function_1_called = true;
        }
    }

    fn function_2(&mut self, students: Vec<String>) {}

    fn f(&mut self, students: Vec<String>) {
        if students.len() < 100 {
            self.function_1(students);
        } else {
            self.function_2(students);
        }
    }
}

fn main() {
    println!("Hello, world!");
    let r = Registration::new();
    // won't compile during `run`:
    // println!("{}", r.function_1_called);
}

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn test_f() {
        let mut r = Registration::new();
        r.function_1(vec!["Alice".to_string(), "Bob".to_string()]);
        assert!(r.function_1_called);
    }
}

代码大致基于您提供的 sn-ps。有一个 Registration 结构,它包含一个学生姓名列表,两个方法 function_1function_2 用于注册学生,还有一个函数 ffunction_1function_2 之间进行选择,具体取决于那里有多少学生是。

在测试期间,Registration 使用附加的布尔变量 function_1_called 进行编译。仅当调用 function_1 时才设置此变量(执行此操作的代码块也标有 #[cfg(test)])。

结合起来,这会在测试期间提供一个额外的布尔标志,以便您可以做出如下断言:

assert!(r.function_1_called);

显然,这可能适用于比单个布尔标志复杂得多的结构(这根本不意味着您应该实际这样做)。

我无法评论这在 Rust 中是否是惯用的。整个设置感觉好像它应该隐藏在花哨的宏后面,所以如果在 Rust 中使用这种测试风格,应该已经有提供这些(或类似)宏的 crate。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-01-25
    • 2017-01-21
    • 2013-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多