Problems sharing test code (traits) via a testutils crate

Hello!

I have a project with two crates: a core crate with common code and a cli crate with "front-end" stuff. These two crates have duplicate test code, so I'd like to share it. I've been looking online and a common suggestion seems to be to add a third testutils crate, which appeals to me because I also want to make the common test code public for consumers of the core library.

So far, I've moved the common code from core into testutils, and then made core have a dev-only dependency onto testutils. For the most part, this seems to work... but I'm having some bizarre trouble with traits.

The core crate provides a BuiltinCommand trait. In testutils, I have defined an InCommand struct that implements such trait. This struct has a new method that returns an Rc<Self>:

// In the testutils crate...
pub struct InCommand {}

impl InCommand {
    pub fn new() -> Rc<Self> { ... }
}

impl core::BuiltinCommand for InCommand { ... }

I want then to consume the InCommand from the unit tests of the core crate... but when I do so, I get this error:

error[E0277]: the trait bound `impl endbasic_core::exec::BuiltinCommand: exec::BuiltinCommand` is not satisfied
   --> core/src/exec.rs:442:26
    |
442 |             .add_command(InCommand::new(Box::from(RefCell::from(golden_in.iter()))))
    |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `exec::BuiltinCommand` is not implemented for `impl endbasic_core::exec::BuiltinCommand`
    |
    = note: required for the cast to the object type `dyn exec::BuiltinCommand`

... but the trait is implemented.

I tried changing the new signature to return an Rc<impl BuiltinCommand> but then I get errors of the form:

error[E0277]: the trait bound `impl endbasic_core::exec::BuiltinCommand: exec::BuiltinCommand` is not satisfied
   --> core/src/exec.rs:442:26
    |
442 |             .add_command(InCommand::new(Box::from(RefCell::from(golden_in.iter()))))
    |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `exec::BuiltinCommand` is not implemented for `impl endbasic_core::exec::BuiltinCommand`
    |
    = note: required for the cast to the object type `dyn exec::BuiltinCommand`

... which make me suspect that something is very wrong with my approach. Note the duality in exec::BuiltinCommand vs. endbasic_core::exec::BuiltinCommand in this error message. It seems as if Rust is picking this trait from two separate places and not realizing they are the same (which in my mind makes sense because the core crate is still supplying the trait definition when its tests are built, so I can see how some confusion could arise there...

Any thoughts? Is my approach doomed?

Thank you!

PS: If this code samples are not specific enough, I can push the actual code as a broken commit somewhere. Let me know.

Here's what's happening under the hood when you run tests for the core crate:

  1. Cargo compiles the core crate as a regular library.
  2. Cargo compiles the testutils crate as a library, using the core library from step 1 as a dependency.
  3. Cargo compiles the core crate again with rustc --test (which produces an executable binary) using the testutils library from step 2 as a dependency.
  4. Cargo runs the core test binary from step 3.

Notice that the core crate is compiled twice (in steps 1 and 3), and the unit test binary uses both copies of the generated code. This is why Cargo is complaining that you have two different traits endbasic_core::exec::BuiltinCommand and exec::BuiltinCommand.

Some possible solutions, depending on your needs:

  • Change these unit tests into integration tests. Then they will link to the core library from step 1, just like the testutils crate does, so both of them will use the same copy of the BuiltinCommand trait. (However, integration tests can't directly test private code from the core library.)

  • Instead of a separate crate, include the testutils code as a module within both the core crate and the cli crate. Then the testutils code will be compiled twice, but it will not introduce any circular dependencies.

  • Rewrite the testutils crate so it does not depend on core. Move code like impl core::BuiltinCommand { ... } into one or both of the core or cli crates. However, this might require duplicating a lot of code.

1 Like

Aha, thanks @mbrubeck. I suspected something like that was happening but didn't know the specifics. And that explains why I did not hit trouble in the integration tests where I shared code.

I'm curious about your second suggestion though as I don't completely follow it. What do you mean by including testutils as a module within both crates? Are you saying to copy/paste the code? Or is there some Cargo feature to do that for me?

You can do this to include a module whose source file is in some other directory:

#[path = "../../path/to/libutils.rs"]
mod libutils;

Very interesting, @mbrubeck. I wasn't aware of that. But... if I do this, what happens during crate publishing time? Does the file get copied and embedded into each crate that references it? (I found Modules - The Rust Reference (rust-lang.org) but it doesn't clarify this.)

No; a packaged crate will only include files that are inside the crate's directory. If you use this strategy for a test-only (#[cfg(test)]) module, then people who download the published crate will not be able to run its tests, but they will still be able to built the crate in non-test mode.

Aha. "me no like" then. But I think your original suggestion of making my tests integration tests isn't as crazy as I felt at first. So will investigate that route. Anyhow, thanks!