Confused by lifetimes and traits for test doubles?


#1

Hello! I’m fairly new to learning Rust, but so far the community and documentation have been excellent in getting me unstuck. Thanks for all your hard work! That said, I seem to have painted myself into a corner that I don’t quite understand.

I’m having trouble writing tests around a piece of code that, in test mode, must not be called (it uses ring 0 instructions). I’ve looked around at a few places, and it seems that the common answer to mocking / stubbing is to extract a trait for the stubbed-out unit and provide an alternate implementation.

Fair enough – I’m curious as to whether there are any hints I can provide to the compiler that in non-test-code there should be exactly one implementation of that trait – but extracting an interface my double can build to is something I’m somewhat used to. So now my code looks something like this:

// The code I'd like to mock
trait Bar {
  fn put(&self, usize);
  fn get(&self) -> usize;
}

// The code I'm testing
struct Foo {
  bar: Bar,
  num: usize,
}

impl Foo {
  fn go(&self) { self.bar.put(self.num) }
}

// The big, scary Bar
struct BigBar { big_bar: usize }
impl Bar for BigBar {
  fn put(&self, u: usize) { panic!("oh no") }
  fn get(&self) -> usize { panic!("oh no") }
}

#[cfg(test)]
struct TestBar { val: usize }

#[cfg(test)]
impl Bar for TestBar {
  fn put(&self, u: usize) { self.val = u }
  fn get(&self) -> usize { self.val }
}


#[test]
fn test() {
  let foo = Foo {
    bar: TestBar { val: 0 },
    num: 42,
  };

  foo.go();

  assert_eq!(foo.bar.get(), 42)
}

I immediately discovered that traits are very different from structs when it comes to sizing:

test.rs:7:3: 7:11 error: the trait `core::marker::Sized` is not implemented for the type `Bar + 'static` [E0277]
test.rs:7   bar: Bar,
            ^~~~~~~~
test.rs:7:3: 7:11 help: run `rustc --explain E0277` to see a detailed explanation
test.rs:7:3: 7:11 note: `Bar + 'static` does not have a constant size known at compile-time
test.rs:7:3: 7:11 note: only the last field of a struct or enum variant may have a dynamically sized type
error: aborting due to previous error

Changing the definition of Bar to extend Sized doesn’t change the error. Following the suggestion and moving Bar to be the last field of the struct just moves the error into the test (and would also seem to limit me to one double-able bit of code per struct):

test.rs:34:13: 37:4 error: the trait `core::marker::Sized` is not implemented for the type `Bar + 'static` [E0277]
test.rs:34   let foo = Foo {
test.rs:35     bar: TestBar { val: 0 },
test.rs:36     num: 42,
test.rs:37   };
test.rs:34:13: 37:4 help: run `rustc --explain E0277` to see a detailed explanation
test.rs:34:13: 37:4 note: `Bar + 'static` does not have a constant size known at compile-time
test.rs:34:13: 37:4 note: required because it appears within the type `Foo`
test.rs:34:13: 37:4 note: structs must have a statically known size to be initialized

(it now comes with a mismatched types complaining about a type mismatch between a Bar + 'static and a TestBar as well, but I presume this is more because of the lifetime than the trait?)

At this point, I think I want to tell rustc that any implementors of Bar will be exactly usize wide. That feels like it gives me the possibility of padding (if the test impl is smaller) or referencing some other state-holding struct (if the test imply is larger) if the size changes in the future.

Unfortunately, the only examples I’ve found in the wild do not specify the size of Bar, but instead move the storage somewhere else entirely and use a reference to the trait instead of the trait itself in Foo. That leads me to believe that perhaps a trait is the wrong answer to my puzzle, but so far my other experiments have had similarly unsatisfactory results:

  • Having two completely distinct Bar definitions, where the impl previously known as BigBar is #[cfg(not(test)) works (and possibly is the most direct expression of what I’m after), but comes at the cost of anything that might touch BigBar (across modules, as in my code it’s part of the public API) must be similarly turned off for test.
  • Using an enum with BigBar and TestBar variants and matching on **self seems like it should work, but complects my production code with my test code and ends up compiling the test code into the production binary.

This has gotten quite long, so:

Summary: I think I want to reserve a ~usize slot in a struct that can be populated with either a production unit or an isolated stub for testing (with ideally zero overhead in the production case), but I suspect there is something fundamental that I’m missing. Is there a simple pattern that others use for overriding parts of a production implementation in test-mode?


#2

Hm, could you use a type alias for the thing?

struct ScaryFoo;
struct TestFoo;

#[cfg(not(test))]
type Foo = ScaryFoo;

#[cfg(test)]
type Foo = TestFoo;

#3

You can also use a generic here

struct FooHolder<FooT: Foo = ScaryFoo> {
    foo: FooT
}

Or you can use a bit of dynamic dispatch

struct FooHolder {
    foo: Box<Foo>
}


#4

Ah ha! It looks like generics with “monomorphization” might be just the tool for the job. In the output test & production artifacts I see just the desired implementations:

$ objdump -dr test | grep -A10 '<.*impl.*put.*>:'
000000000000e450 <_ZN13_$LT$impl$GT$3put20h7b600fa4c2cc2234GcaE>:
    e450:	c6 44 24 ff 3d       	movb   $0x3d,-0x1(%rsp)
    e455:	48 89 7c 24 f0       	mov    %rdi,-0x10(%rsp)
    e45a:	48 89 74 24 e8       	mov    %rsi,-0x18(%rsp)
    e45f:	48 8b 74 24 f0       	mov    -0x10(%rsp),%rsi
    e464:	48 8b 7c 24 e8       	mov    -0x18(%rsp),%rdi
    e469:	48 89 3e             	mov    %rdi,(%rsi)
    e46c:	c3                   	retq

And the non-test method is much longer but similarly just what I want:

$ objdump -dr main | grep -A10 '<.*impl.*put.*>:'
00000000000054e0 <_ZN13_$LT$impl$GT$3put20hb50c57878f9e31d61aaE>:
    54e0:	48 81 ec b8 00 00 00 	sub    $0xb8,%rsp
    ...
    55c6:	e8 85 0b 00 00       	callq  6150 <_ZN10sys_common6unwind16begin_unwind_fmt20hef3fd9b04428ea45ixsE>

(it seems that “sys_common begin_unwind” is the actual function that implements the panic! macro)

Thank you very much for your help! If anyone wants to try the test doubling code I ended up with, it’s here (though I don’t know how to run tests on play.rust-lang.org): https://play.rust-lang.org/?gist=dce75059a6f57089de96&version=stable