When a Move module can’t be tested from outside — the missing-constructor trap
There’s a good thread here already on how to reach struct fields in tests (the answer being #[test_only] accessors). I ran into the opposite side of that problem recently and haven’t seen it written up, so here it is.
The short version: some modules can’t be exercised from an external test at all — not because your test is wrong, but because there’s no way to construct the type the entry functions demand. And you often don’t find out until you’ve already burned a lot of time trying.
Here’s how I hit it. I was building a test-generation tool and running it against real protocol code to see how it holds up — one of the targets was a staking/farm module from a published DeFi library(SuiTears). Its abort paths all looked reachable on paper: start-time checks, account-mismatch guards, insufficient-amount checks. Five abort sites, all things you’d want tests for.
The problem: the entry point needs a Farm object, and to build a Farm you need &CoinMetadata<StakeCoin>. CoinMetadata isn’t something a test can just make up — it comes from coin::create_currency, which requires a one-time witness. A test module can define its own OTW, so in theory the chain is:
OTW → create_currency → CoinMetadata → new_farm → new_account → stake → reach the deeper guards
In practice every approach I tried to assemble that chain failed to compile. And the module shipped without any #[test_only] constructor for Farm, so there was no shortcut. The existing test files in that codebase didn’t cover the farm module either — which, once I understood why, made complete sense. It wasn’t an oversight on my end or theirs; the type was just genuinely hard to construct in a test context, and nobody had added the helper that would make it possible.
So the five abort paths are, as the module ships, unreachable by any external test. Not “hard to test” — unreachable. That’s a different category, and it’s worth naming because the fix and the diagnosis are different from a normal coverage gap.
A couple of things I took from it:
This is a real testability property, not a skill issue. If you’re auditing or reviewing a module and its guards can’t be reached from a test, that’s a finding on its own — it means those paths ship unverified, and it means anyone who later wants to test them has to patch the upstream module first. Worth flagging in a review rather than quietly skipping.
The #[test_only] accessor pattern from the other thread has a constructor-side cousin. Everyone knows to add #[test_only] getters so tests can read internal state. The same courtesy applies to construction: if your type can only be built through an OTW-gated currency setup (or any similarly heavy chain), a #[test_only] constructor like
#[test_only]
public fun new_for_testing<...>(...): Farm<...>
is what lets external tests reach the guards at all. In the same codebase, other modules that did expose testing constructors (an oracle module had a new_..._for_testing, and an owner module took a generic witness) were straightforward to test from outside. The presence or absence of that one helper decided whether the module was testable at all.
A cheap pre-check saves the wasted rounds. Before writing (or generating) tests for a module, it’s worth scanning: for each struct an entry function requires, is there any constructor a test can reach — public or #[test_only]? If not, the abort paths behind it aren’t testable yet, and you know that up front instead of after several failed attempts.
I ended up baking that pre-check into the tooling I was working on, and I opened an issue on the upstream library suggesting the missing #[test_only] constructors — but the pattern itself is general, so I wanted to put it here for anyone hitting the same wall.
Curious whether others have run into modules that are structurally untestable as shipped, and how you handle it — flag-and-skip, fork-and-patch, or push the helper upstream?