As pointed out in the comments, field/function visibility is a concept that only exists at compile time, so you will need to find a way to test that some code fails to compile.
Doctests are a obvious tool, but are somewhat of a sledgehammer solution. I'd recommend the trybuild crate instead. I've mostly seen it used by macro authors, but it can generally be useful whenever you want to test that something either does or doesn't compile.
An example usage might be:
// src/lib.rs (could be anywhere really, it just needs to be a regular #[test] function) #[cfg(test)] #[test] fn ui_tests() { let t = trybuild::TestCases::new(); t.compile_fail("tests/fail/*.rs"); } // tests/fail/private_field_access.rs fn main() { let my_struct = MyStruct::new(); println!("{}", my_struct.private_field); }
This will generate a corresponding .stderr file that you can check into version control (similar to rustc's "ui" tests). When the tests are run, the file is compiled, and the output from the compiler is compared against the actual error.
This means you can catch regressions in a more fine-grained way than a simple binary "does it compile" check. For example, if you accidentally made the private field public, the above test may still fail to compile for some other reason (perhaps it doesn't implement Display). trybuild catches those regressions in a way that compile-fail doctests cannot.
When it comes time to generate the .stderr files, you can run TRYBUILD=overwrite cargo test to overwrite the existing files.
It's worth mentioning that trybuild tests (like doctests) only work for the external API of your crate, not module. If that's an issue, you could consider using cargo workspaces and a multi-crate setup (there are other reasons this might be preferable, e.g. compile times).
compile_failattribute to assert that the method is not accessible outside the crate.