Move the computation of the result and the printing in two separate functions. Have the first function return the result. Only test this first function, by directly comparing the return value to the expected result, without converting either of them to a string in any way.
If the printing is a core part of your logic, or so intimately coupled with your code that it would be hard to extract in the way @H2CO3 suggested, you could pass in some object which receives things it should print.
For example, we might define a Logger trait and create an implementation which just forwards to println!():
And during testing you might create a DummyLogger which just turns the messages into a string and saves it in a Vec.
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct DummyLogger(Vec<String>);
impl Logger for DummyLogger {
fn print(&mut self, value: &Arguments<'_>) {
self.0.push(value.to_string());
}
}
#[test]
fn some_test() {
let mut logger = DummyLogger::default();
my_complicated_function(&mut logger);
assert_eq!(logger.0[2], "Hello, World!");
}
Threading the Logger through to each nested function that needs it might get annoying, but normally I'll interpret that as my code telling me "maybe things are too coupled and you should find a simpler way to structure this code".
This general technique is often referred to a "Dependency Injection".