| name | crane-testing |
| description | This skill should be used when the user asks about "testbase", "behavior library", "invariant test", "handler", "fuzz test", "test pattern", "Behavior_", "TestBase_", "mock", "vm.mockCall", "unit test", "write a test", "CraneTest", or needs guidance on Crane's testing infrastructure for writing comprehensive smart contract tests. Prefer production code over mocks. |
| license | MIT |
Crane Testing Patterns
Crane uses structured testing patterns with TestBase contracts, Behavior libraries, and Handler contracts for comprehensive coverage. All patterns must comply with LR-7 (see PRD) and LR-1 (NatSpec on test code).
Production-first testing (read this first)
Prefer production code and production deploy paths. Do not invent mocks for the subject under test (SUT).
Generic Foundry skills that show MockOracle, vm.mockCall, or new MyContract() are subordinate to this skill for Crane work.
Ladder (use the highest step that fits)
- Real production contracts, deployed via
CraneTest factories (create3Factory, diamondPackageFactory) — same path as production. No address(0) facets in DFPkg init.
- Existing
TestBase_* chains. Search contracts/**/TestBase_*.sol before writing setup from scratch.
- External protocols: protocol ports under
contracts/protocols/.../stubs/ (real/protocol-faithful code for hermetic deploy) or fork bases (TestBase_*Fork + live addresses). Do not invent interface mocks when a TestBase already deploys the protocol.
- Harness doubles outside the SUT only when they implement real interfaces and add controllability (mintable ERC20, reentrancy token, Behavior targets like
ERC20TargetStub).
vm.mockCall / fake contracts only for non-SUT isolation, failure modes production cannot express cheaply, or documented oracle/VRF harnesses — SUT remains real.
Forbidden for SUT
- Mocking facets, DFPkgs, diamond proxies, Create3/diamond factories, or registries under test
- Replacing a protocol integration with a canned mock when
TestBase_* + protocol ports or forks exist
- Deploying production Crane facets/DFPkgs with
new (use factories; protocol ports in TestBases may use new for the ported protocol code only)
Terminology (do not conflate)
| Term | Meaning |
|---|
Protocol port (*/stubs/) | Real/protocol-faithful implementation for hermetic local deploy (e.g. CamelotFactory) — not a fake |
| Harness / mintable stub | Minimal ERC20/target with test hooks |
| Mock / test double | Canned dependency behavior; last resort; never the SUT |
| Handler | Fuzz/invariant wrapper around a real SUT |
See also docs/development/testing.md and consumer project AGENTS (e.g. IndexedEx indexedex-testing) for layered registry rules.
Critical Alignment Requirements (LR-3):
- Skills reflect finalized standards: full initialization, exact value assertions, mandatory Behavior library usage (not ad-hoc asserts), Facet + Package declaration tests, correct CraneTest/TestBase_* setUp() call order, NatSpec on all test surfaces (TestBase, Behavior, Handler, *.t.sol per LR-1 scope).
- Ties to central NatSpec process: use
CENTRALLY_COMPUTED_NATSPEC_VALUES.md exclusively for any example selectors/interfaceIds (e.g. IFacet: facetName 0x5b6f4d01, facetInterfaces 0x2ea80826, facetFuncs 0x574a4cff, facetMetadata 0xf10d7a75, supportsInterface 0x01ffc9a7). Values originate from dedicated Foundry Script verification (not ad-hoc cast). See crane-natspec skill + docs/development/natspec.md.
- ERC1967 awareness: tests involving storage (multi-instance, custom slots) must respect correct
bytes32(uint256(keccak256(abi.encode("name"))) - 1) DEFAULT_SLOT/STORAGE_SLOT pattern (see crane-architecture skill, LR-6).
- References GitBook / LR-2 required content: cross-link
docs/development/testing.md; tests must cover required LR-2 areas (registry population assertions, protocol TestBase usage for ports, utility libs like Sets + ConstProdUtils in integration tests).
Test Directory Structure
Test infrastructure lives in contracts/, test specs live in test/:
contracts/ # Test infrastructure WITH the code
├── access/ERC8023/
│ ├── MultiStepOwnableRepo.sol
│ ├── MultiStepOwnableFacet.sol
│ └── TestBase_IMultiStepOwnable.sol # TestBase next to implementation
├── introspection/ERC165/
│ ├── ERC165Facet.sol
│ ├── TestBase_IERC165.sol # Behavior testing
│ └── Behavior_IERC165.sol # Validation library
└── test/
├── stubs/ # Small harnesses / examples (not protocol ports)
├── comparators/ # Assertion helpers
└── behaviors/ # Shared behavior utilities
# Protocol ports (real protocol code for hermetic deploy) live under:
# contracts/protocols/.../stubs/ — e.g. CamelotFactory, not interface mocks
test/foundry/spec/ # Actual test specs mirror contracts/
├── access/ERC8023/
│ └── MultiStepOwnable.t.sol
└── introspection/ERC165/
└── ERC165Facet.t.sol
TestBase Pattern
Two types of TestBase contracts exist. All TestBases MUST follow LR-7 rules.
Correct CraneTest / TestBase Inheritance and Initialization Order (LR-7)
- Always inherit
CraneTest (or a TestBase_* chain that ultimately inherits it) for factory bootstrap.
- Call parent
setUp() via Super.setUp() (or explicit parent) first in override.
- Full and Correct Initialization required: Never pass
address(0) for facet addresses in DFPkg construction or package deployment. The subject under test must reach a production-like, fully-initialized state before any assertions (see InitDevService, CraneTest.setUp()).
- Deploy only if not already present (idempotent guards) to support inheritance chains.
Protocol Setup TestBase
Sets up protocol infrastructure with inheritance chains. new CamelotFactory / new CamelotRouter here deploy protocol ports (production-faithful code under protocols/.../stubs/), not canned mocks. Crane facets/DFPkgs under test still go through factories.
abstract contract TestBase_CamelotV2 is TestBase_Weth9 {
ICamelotFactory internal camelotV2Factory;
ICamelotV2Router internal camelotV2Router;
function setUp() public virtual override {
TestBase_Weth9.setUp(); // Call parent setUp FIRST (CraneTest provides factories)
if (address(camelotV2Factory) == address(0)) {
// Protocol port under contracts/protocols/dexes/camelot/v2/stubs/
camelotV2Factory = new CamelotFactory(feeToSetter);
}
if (address(camelotV2Router) == address(0)) {
camelotV2Router = new CamelotRouter(address(camelotV2Factory), address(weth));
}
}
}
See GitBook LR-2 content: docs/development/testing.md (Setup TestBases) and protocol docs for how TestBases enable testing of ported protocols (e.g. Balancer, Camelot) + utility usage.
Behavior TestBase
Defines expected behavior via virtual functions. Used for declaration tests:
abstract contract TestBase_IFacet is Test {
IFacet internal testFacet;
function setUp() public virtual {
testFacet = facetTestInstance(); // Must be fully initialized instance (no 0 addr)
}
// Virtual functions - inheritors return expected (control) values
function facetTestInstance() public virtual returns (IFacet);
function controlFacetName() public view virtual returns (string memory);
function controlFacetInterfaces() public view virtual returns (bytes4[] memory);
function controlFacetFuncs() public view virtual returns (bytes4[] memory);
// Test functions validate actual vs expected using Behavior (exact)
function test_IFacet_facetName() public view {
assertTrue(Behavior_IFacet.areValid_IFacet_facetName(testFacet, controlFacetName(), testFacet.facetName()));
}
function test_IFacet_FacetInterfaces() public {
bytes4[] memory expected = controlFacetInterfaces();
bytes4[] memory actual = testFacet.facetInterfaces();
assertEq(actual.length, expected.length, "count mismatch");
assertTrue(Behavior_IFacet.areValid_IFacet_facetInterfaces(testFacet, expected, actual));
}
function test_IFacet_FacetFunctions() public {
// ... similar exact length + Behavior
}
function test_IFacet_FacetMetadata_Consistency() public {
assertTrue(Behavior_IFacet.isValid_IFacet_facetMetadata_consistency(testFacet));
}
}
Facet Declaration Tests (mandatory LR-7): Every deployed Facet must be tested via facetInterfaces(), facetFuncs(), facetName(), facetMetadata() matching expected (via Behavior + control fns). Use central IFacet selectors when asserting or documenting.
Package Declaration Tests (mandatory LR-7): DFPkgs must be tested for packageName(), facetAddresses(), facetCuts(), diamondConfig(), calcSalt(...), processArgs(...), initAccount(...), postDeploy(...). End-to-end via real factory delegatecall for initAccount.
See TestBase_IFacet + Behavior_IFacet (contracts/factories/diamondPkg/). Similar for IDiamondFactoryPackage.
Behavior Libraries (Behavior_*.sol)
Mandatory LR-7 Rule: Standard interfaces and patterns (IFacet, IDiamondFactoryPackage, protocol interfaces) MUST be validated using the appropriate Behavior_* libraries (e.g. Behavior_IFacet, Behavior_IERC165, Behavior_IDiamondFactoryPackage). Hand-written assertions or direct assertEq on these surfaces are insufficient and non-compliant.
Libraries encapsulate validation logic for interface compliance. Named Behavior_I{Interface}:
library Behavior_IERC165 {
using UInt256 for uint256;
Vm constant vm = Vm(VM_ADDRESS);
function _Behavior_IERC165Name() internal pure returns (string memory) {
return type(Behavior_IERC165).name;
}
// expect_* - Store expected values in ComparatorRepo (use before subject calls in tests)
function expect_IERC165_supportsInterface(IERC165 subject, bytes4[] memory expectedInterfaces_) public {
console.logBehaviorEntry(_Behavior_IERC165Name(), "expect_IERC165_supportsInterface");
Bytes4SetComparatorRepo._recExpectedBytes4(
address(subject), IERC165.supportsInterface.selector, expectedInterfaces_
);
console.logBehaviorExit(_Behavior_IERC165Name(), "expect_IERC165_supportsInterface");
}
// isValid_* / areValid_* - Compare expected vs actual directly (preferred in declaration tests)
function isValid_IERC165_supportsInterfaces(IERC165 subject, bool expected, bool actual)
public view returns (bool valid)
{
valid = expected == actual;
if (!valid) {
console.logBehaviorError(...);
}
return valid;
}
// hasValid_* - Validate against stored expectations (for post-action checks)
function hasValid_IERC165_supportsInterface(IERC165 subject) public view returns (bool isValid_) {
console.logBehaviorEntry(_Behavior_IERC165Name(), "hasValid_IERC165_supportsInterface");
// ... iteration over expectations
console.logBehaviorExit(_Behavior_IERC165Name(), "hasValid_IERC165_supportsInterface");
}
}
Behavior Function Types
| Pattern | Purpose | Example |
|---|
expect_* | Store expected values | expect_IERC165_supportsInterface(subject, interfaces) |
isValid_* / areValid_* | Compare expected vs actual directly (exact match) | isValid_IERC165_supportsInterfaces(subject, true, actual) |
hasValid_* | Validate against stored expectations | hasValid_IERC165_supportsInterface(subject) |
Behavior libs produce structured console logs (logBehavior*) for debugging mismatches. They are used in both unit declaration tests and inside Handlers.
See GitBook: docs/development/testing.md (Behavior Libraries) + LR-2 protocol/utility test coverage. Also Behavior_IFacet for facet declaration (uses central IFacet values 0x5b6f4d01 etc.). Cross-ref crane-natspec for NatSpec on Behavior public helpers.
Handler Pattern (Invariant Testing)
For fuzz/invariant testing, use a Handler + TestBase pattern per LR-7.
Handler Contract
Wraps Subject Under Test (SUT), exposes fuzzable operations, tracks expected state. Must use exact assertions + declared expectations:
- Always use
vm.expectRevert(...) (selector + typed form where possible; match NatSpec selector) and vm.expectEmit before state changes.
- Track precise expected state (e.g.
_expectedAllowance).
- Normalize seeds to small address set via
addrFromSeed.
- SUT must be attached in fully initialized state (production-like).
contract ERC20TargetStubHandler is Test {
IERC20 public sut;
mapping(bytes32 => uint256) internal _expectedAllowance; // exact tracked state
function transfer(uint256 ownerSeed, uint256 toSeed, uint256 amount) external {
address owner = addrFromSeed(ownerSeed); // Normalize fuzz input
address to = addrFromSeed(toSeed);
uint256 bal = sut.balanceOf(owner);
vm.prank(owner);
if (amount > bal) {
vm.expectRevert(IERC20Errors.ERC20InsufficientBalance.selector); // exact
sut.transfer(to, amount);
return;
}
vm.expectEmit(true, true, false, true); // precise topics + data
emit IERC20.Transfer(owner, to, amount);
sut.transfer(to, amount);
// handler would update its _expected* here for invariants
}
}
Invariant TestBase
Declares invariants and virtual deployment functions. Invariants use exact assertEq (no "changed" checks):
abstract contract TestBase_ERC20 is Test {
ERC20TargetStubHandler public handler;
function _deployToken(ERC20TargetStubHandler handler_) internal virtual returns (IERC20);
function setUp() public virtual {
handler = new ERC20TargetStubHandler();
IERC20 token = _deployToken(handler); // full init, no bypass
handler.attachToken(token);
targetContract(address(handler));
targetSelector(FuzzSelector({
addr: address(handler),
selectors: [handler.transfer.selector, handler.approve.selector]
}));
}
// Invariant: exact equality
function invariant_totalSupply_equals_sumBalances() public view {
address[] memory addrs = handler.asAddresses();
uint256 sum = 0;
for (uint256 i = 0; i < addrs.length; i++) {
sum += handler.balanceOf(addrs[i]);
}
assertEq(sum, handler.totalSupply(), "exact totalSupply invariant");
}
// Preview parity example (LR-7 for vaults/etc.): assert exact match preview vs execute
// function test_preview_matches_execute() ... { uint256 p = vault.previewX(..); ... assertEq(p, actual); }
}
Additional LR-7 invariants expectations:
- CREATE3/salt determinism assertions (same inputs => same addr).
- Registry population assertions after deployments (FacetRegistry etc. contain expected).
- Storage isolation proofs for multi-instance.
- Full access-control + reentrancy matrix using actual guards (if using Operable/Reentrancy).
- Fork parity for protocol ports (compare outputs to live on-chain).
- Preview function exact parity for all preview* (deposit/mint/withdraw/redeem etc.).
TestBase Inheritance Chain
CraneTest # Factory setup via InitDevService (create3Factory, diamondPackageFactory) - call setUp() first
└── TestBase_Weth9 # WETH deployment
└── TestBase_CamelotV2 # Camelot factory + router
└── TestBase_CamelotV2_Pools # Pool creation helpers
└── YourTest.t.sol # Actual test contract
Rule (LR-7 + CraneTest order): Inheritors must call the parent setUp in correct order. CraneTest.setUp bootstraps via InitDevService.initEnv (provides create3Factory + diamondPackageFactory). See crane-deployment skill for CraneTest + factory usage inside tests. Never new real production Crane contracts in tests (use factories + packages).
See GitBook LR-2: docs/development/testing.md + deployment docs for registry + protocol TestBase usage.
Key Conventions (LR-7)
- Production-first: real contracts + factories; no mocks for SUT (see top of this skill).
- Full init only: SUTs (especially via DFPkgs) use real non-zero facet addresses; no bypass.
- Handler normalizes fuzz inputs:
addrFromSeed(seed) maps to small address set.
- Handler tracks expected state:
_expectedAllowance, _seen, etc.
- Exact assertions always:
assertEq(expected, actual) (precise deltas), never "changed" checks. Use vm.expectEmit with full topics/data.
- Invariant functions named
invariant_* for Foundry discovery.
- Use
vm.expectRevert (selector + typed) / vm.expectEmit to declare expected behavior. Match NatSpec selectors.
- TestBase declares virtual
_deploy* functions for SUT injection.
- Mandatory Behaviors for IFacet / IDiamondFactoryPackage / standards (see above).
- Declaration tests for every Facet + Package.
- Preview parity exhaustive where applicable.
- Registry + CREATE3 determinism + storage isolation assertions required after relevant ops.
- NatSpec on test code (LR-1 scope): public APIs on TestBase_, Behavior_, IHandler, handlers, stubs, comparators, *.t.sol must use // tag:: + @custom:selector etc. Use ONLY central values.
Additional Resources
Reference Files
references/behavior-library.md - Complete Behavior library guide
references/handler-pattern.md - Invariant testing with Handlers
Key Files (cross-ref AGENTS.md)
/contracts/test/CraneTest.sol - Base with factory infrastructure (via InitDevService)
/contracts/factories/diamondPkg/TestBase_IFacet.sol + /contracts/factories/diamondPkg/Behavior_IFacet.sol - Facet declaration + Behavior
/contracts/introspection/ERC165/Behavior_IERC165.sol + TestBase_IERC165.sol
/contracts/tokens/ERC20/TestBase_ERC20.sol + handler - Invariant example
/contracts/test/comparators/Bytes4SetComparator.sol + other comparators
/contracts/test/behaviors/BehaviorUtils.sol
- Protocol:
contracts/protocols/.../test/bases/TestBase_*.sol
- Also see
IHandler.sol, stubs under contracts/test/
Ties to other skills: crane-deployment (factories + init in tests), crane-architecture (DFPkg + slots), crane-natspec (test NatSpec + central values process). GitBook: docs/development/testing.md, docs/development/natspec.md, deployment/ and protocols/ sections (LR-2).
LR-7 Testing Standards (Mandatory - Full List Excerpt)
All tests (unit, behavior/declaration, invariant, fork) must satisfy (non-exhaustive per PRD):
- Full and Correct Initialization - Fully initialize before assertions. Packages use real facets (no address(0)). Production-like state only. Prefer production code over mocks for the SUT.
- Exact Expected Value Assertions - Precise values/deltas (e.g.
assertEq(delta, expectedAmount)), not side-effect "changed".
- Preview Function Exact Match - For previewDeposit etc., assert returned value exactly equals result of executing the action (incl. edges).
- Facet Declaration Tests - Validate
facetInterfaces(), facetFuncs(), facetName(), facetMetadata() using Behavior_IFacet + control values.
- Package Declaration Tests - Validate all pkg metadata +
calcSalt, processArgs, initAccount (real delegatecall), postDeploy.
- Mandatory Behavior Libraries - Use
Behavior_* (IFacet, IDiamondFactoryPackage, protocols) for standards compliance. No bypassing.
- Event + Delta Exactness -
vm.expectEmit + exact post-state.
- CREATE3 / Salt Determinism - Assert identical inputs give identical addresses.
- Registry Population - After deploy via factory/pkg, assert registries (Facet/Package/CallTarget etc.) updated correctly.
- Storage Isolation - Multi-instance tests prove no leakage.
- Full DFPkg Lifecycle - calcSalt, process, delegatecall initAccount, postDeploy.
- Error Selectors - Test with selector and typed form; match declared NatSpec selector (use central values).
- Correct TestBase/CraneTest - Proper inheritance + ordered
setUp() calls; no duplication/bypass.
- NatSpec on Test Code - Same LR-1 standard for public test APIs (tags + @custom using central).
- Access/Reentrancy Matrix + Fork Parity - Full coverage for operable/reentrancy; fork tests compare key outputs to on-chain.
Tests violating these are insufficient. See also exact registry + fork details in PRD LR-7 and AGENTS.md.
Central NatSpec + ERC1967 Ties for Testers
- Use
CENTRALLY_COMPUTED_NATSPEC_VALUES.md (ONLY) when tests or Behaviors reference selectors/interfaceIds (e.g. IFacet values above).
- NatSpec verification is via committed Foundry Script (see
docs/development/natspec.md + scripts/).
- Storage-using tests must align with ERC1967 slot standard (LR-6):
DEFAULT_SLOT = bytes32(uint256(keccak256(abi.encode("hier.name"))) - 1).
- This enables agent-proof reuse + GitBook areas (LR-2/LR-4): reusable factories, declared facets, testable protocols.
Update this skill (and its gap report) whenever standards evolve (LR-3).