Our team decided to open source this as we think it could benefit the whole rust community. Also we are seeking feedback from the community to make it better: injectorpp
In short, injectorpp allows you to mock functions without using trait.
For example, to write tests for below code:
fn try_repair() -> Result<(), String> {
if let Err(e) = fs::create_dir_all("/tmp/target_files") {
// Failure business logic here
return Err(format!("Could not create directory: {}", e));
}
// Success business logic here
Ok(())
}
In my current career as a software developer, I saw most if not all the articles are asking developers to use interfaces to make the code unit testable. After seeing many "interfaces" or "traits" that are used solely for testing purpose and made the project complex, I start to doubt it. Is the test only interface really worth it? Should the interface be used for business need instead of only for making code unit testable? Can we have a way to test our code without introducing these tests only interfaces?
I'm curious, is there any writeup on what mechanism this uses? It seems to me like the only way to do this would be to patch code in memory, which would depend on debug info, be unreliable if inlining happens and is almost certainly undefined behaviour.
Or did you figure out something else that I missed?
Pretty gnarly low level stuff, they also document recommend profile.test settings in the readme to help it work so I think you basically have the right idea. Hopefully not instant undefined behaviour, I am not gonna suggest it does or doesn't as I simply don't know
In high level concept it's like implementing a JIT compiler. The same function could have different machine code in different platforms at runtime. Here the "platforms" are production and test. In production, the machine code is not changed. In test, the machine code is different.
I would say it does not depend on debug info except the inline. Afterall the memory address of a function needs to be there at the beginning. That's why there's a recommended [profile.test] setting to prevent optimization.
Besides that, as long as the function memory address can be captured, the rest implementation is pretty much well defined behavior. Yeah it's a JIT compiler to translate the function to different machine code.
Yeah you get the point. The only prerequisite right now is the memory address of a function. That's why [profile.test] settings are recommended. Besides that, the low-level implementations are conceptually similar like implementing a JIT compiler. Translate the function to different machine code according to your platforms. As long as we know what the machine code is doing, it's not undefined behavior. Similar concept to how JIT compilers are implemented
I forgot to mention, one of the big challenges that solved by injectorpp in our team is faking system functions. Projects rely on low-level system APIs always have such challenges.
Below is an example for faking shm_open
#[test]
fn test_fake_shm_open_should_return_fixed_fd() {
// Fake shm_open to always return file descriptor 32
let mut injector = InjectorPP::new();
injector
.when_called(injectorpp::func!(shm_open))
.will_execute(injectorpp::fake!(
func_type: fn(_name: *const c_char, _oflag: c_int, _mode: c_uint) -> c_int,
returns: 32
));
let name = CString::new("/myshm").unwrap();
let fd = unsafe { shm_open(name.as_ptr(), 0, 0o600) };
assert_eq!(fd, 32);
}
Thanks for sharing!
Did you experience using it to mock other of the typical external dependencies in a project, i.e database crates? Just like the way you mock fs functions, i'm evaluating possibilities to mock other dependencies.