How to check if a function has been called in Rust

2020-03-30 16:57发布

I have a function as follows

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);
    }
}

In unit tests, I am planning to check whether function_one or function_two was called.

#[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"));
        },
    );
}

How can I test if a function was called in Rust?

2条回答
疯言疯语
2楼-- · 2020-03-30 17:22

Here is a naïve attempt using #[cfg(test)] in multiple places:

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);
    }
}

The code is loosely based on the snippets that you provided. There is a Registration struct that holds a list of student names, two methods function_1 and function_2 for registering students, and a function f that chooses between function_1 and function_2 depending o how many students there are.

During tests, Registration is compiled with an additional Boolean variable function_1_called. This variable is set only if function_1 is called (the block of code that does that is also marked with #[cfg(test)]).

In combination, this makes an additional Boolean flag available during the tests, so that you can make assertions like the following one:

assert!(r.function_1_called);

Obviously, this could work for structures much more complicated than a single boolean flag (which does not at all mean that you should actually do it).

I cannot comment on whether this is idiomatic in Rust or not. The whole setup feels as if it should be hidden behind fancy macros, so if this style of testing is used in Rust at all, there should already be crates that provide those (or similar) macros.

查看更多
ら.Afraid
3楼-- · 2020-03-30 17:27

Strong opinion alert: you are doing your testing wrong. This is on the same level as "how do I test a private method". You shouldn't care about the implementation of registration to this level of detail.

That being said, if it's actually important to know which if branch is taken, then use dependency injection:

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

Once you have done this, then you can see that registration calls the appropriate trait function (as shown in the example test).

This prevents your production code from having test-specific detritus strewn about while also giving you the ability to be more flexible and test more cases rapidly.

See also:

查看更多
登录 后发表回答