2

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?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
not 0x12
  • 19,360
  • 22
  • 67
  • 133
  • I guess that your functions have a side effect, you can check this effect. – Boiethios Feb 07 '19 at 21:48
  • I have absolutely no idea what it is or what it does, but the project name looks somewhat promising: [mock_derive](https://github.com/DavidDeSimone/mock_derive). – Andrey Tyukin Feb 07 '19 at 22:30
  • Follow-up of https://users.rust-lang.org/t/rust-how-to-check-whether-a-function-being-called-or-not/25045 – hellow Feb 08 '19 at 07:55

2 Answers2

11

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:

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • 2
    I guess your opinion depends on the language you are most familiar with, unit tests should test the functionality not the other functions , Intention here was to test whether other function being called from the method – not 0x12 Feb 08 '19 at 02:17
  • 2
    @Kalanamith Since you are not simply testing the external behavior of your function (comparing the return result), but you also want to see whether a certain other method is invoked, you are essentially testing the side effects (because the method invocation by itself is a sort of "side-effect", albeit a rather strange one). The usual practice when dealing with any kind of side-effects in tests would be to make the function *generic over an interpreter that performs the side effects*, and then to *inject* a special test-interpreter during the tests. – Andrey Tyukin Feb 08 '19 at 02:29
  • 2
    @Kalanamith In this sense, what Shepmaster proposed here is much closer to what I would usually do when writing my Scala code (this approach seems increasingly language agnostic, and applicable in many different ecosystems). I tried the `cfg`-approach mostly because I was curious whether it would compile at all, not because I would suggest it as a best practice. – Andrey Tyukin Feb 08 '19 at 02:31
  • @AndreyTyukin Yep, Agree. – not 0x12 Feb 08 '19 at 02:41
  • 1
    This bit `impl Registration for &'_ mut R` looks a bit like black magic. Can you add a little on why its needed and what it does? – Michael Anderson Feb 08 '19 at 02:44
  • Also - Can you avoid the need for changing `&self` to `&mut self` in the signature by using a RefCell for interior mutablity? – Michael Anderson Feb 08 '19 at 02:45
  • @MichaelAnderson yes, interior mutability is a good and common solution when your test implementations need mutability but your real code does/can not. That implementation is just [implementing the trait for all references to types that implement the trait](https://stackoverflow.com/q/28799372/155423). That's what allows us to pass in `&mut reg` in the test while holding onto our own copy. You could also use something like an `Rc` / `Arc` to keep a shared reference (and then you *need* to use interior mutability). – Shepmaster Feb 08 '19 at 03:04
  • @Shepmaster In the accepted answer to the last question you linked, it is said: *"For this reason, taking the parameter to `runner()` by value is probably undesirable; you should instead be taking it by reference."* Wouldn't it also apply here if `mut registration: impl Trait` is replaced by `registration: &mut impl Trait`? Looks as if it could simplify the example a little bit. – Andrey Tyukin Feb 08 '19 at 13:55
  • 1
    @AndreyTyukin you are right that it would simplify this example, but I'd say that's largely because there's only the test code to look at. In a bigger example, you'd want to prioritize the production code. Maybe you'd want the `R` to be owned by the struct containing the method `registration` so that it can be returned from a function that created `R`. See the semi-related [API guideline about taking `Read` / `Write` by value](https://rust-lang-nursery.github.io/api-guidelines/interoperability.html#generic-readerwriter-functions-take-r-read-and-w-write-by-value-c-rw-value). – Shepmaster Feb 08 '19 at 14:25
6

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.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93