Integrated tests

Good testing practice is something most companies and hobby programmers still don't really apply. The reason is pretty obvious: To save the work it would take. In a private context it's fine in the majority of cases, you mostly just don't have that high requirements for the correctness of your program. You might even use a dynamically typed and/or script language for that reason. On the other hand, companies often handle it that way out of shortsightedness. The extra work is "wasted" in that it directly contributes zero bucks to the operating income. Their value is invisible, right up to the point where something got changed and the software suddenly breaks, at which point tests can either tell what exactly broke, or more importantly, can prevent breakage at all.

Back to the "amount of work" thing, setting up testing can be really cumbersome in some languages. I recently helped a friend setting tests up in C#, we needed multiple hours because of a version difference since Visual Studio 2019 installed .NET at version 5.0, whereas the test framework used version 4.8. Using a testing framework and setting everything up for testing can be quite an amount of work and complicate a project even further, so having an easy way to test things is quite valuable in my opinion.

In Rust, testing is as easy as it gets. Simply write a function without parameters or return value, annotate it with #[test] and you have created a unit test! It can be tested by the default toolchain supplied with Rust in practically all cases by using the command cargo test. It is recommended to put unit tests for a module into a submodule. As you might remember from the scope chapter, the module has access to private items of the module containing it. Also, you can annotate the module with #[cfg(test)], so it only gets compiled into your crate when you're testing.

struct MyStruct {
  private_field: usize,
}

#[cfg(test)]
mod tests {
  #[test]
  fn a_test() {
    // We have access to private items!
    let value = MyStruct {
      private_field: 42
    };
    assert_eq!(value.private_field, 42);
  }
}

Now this was nice, but that was only about unit tests, what about integration tests? For that, you can create a tests folder right next to the src folder. Cargo will automatically interpret each program file inside the tests folder as an integration test module and output its tests in a separate listing when executing them. Those tests will see your crate as just as external as any dependency you're using inside it, so you only have access to your public API. Also, in order to have common code between integration test modules, you can create a subfolder inside the tests directory and put a mod.rs program file inside it. This way, the file doesn't sit directly inside the tests folder and thus isn't interpreted as an integration test module itself. Instead, each integration test module will see it as a submodule of itself. Your project directory structure then may look like this in total:

  • src
    • some_module
      • inner_module.rs
    • some_module.rs
    • lib.rs
  • test
    • common
      • mod.rs
    • some_use_case.rs
    • another_use_case.rs

So we had unit tests and integration tests already, but there is another interesting thing about tests not mentioned yet. Remember that you can put example code inside your documentation? Well, this example code gets tested too! This is extremely valuable, since it not only can detect your normal crate code breaking, but also your documentation's. As it doesn't directly contribute to the crates's functionality, it is way easier to forget updating it when changing something inside the crate.