Attributes — Testing Philosophy¶
Where the tests live¶
Tests/EditMode/Attributes/
AttributeSetValueTests.cs 12 cases — ids, writes, bounds, the sign convention
AttributeSetEventTests.cs 8 cases — both values, emit order, listener isolation
AttributeSetCombinerTests.cs 16 cases — providers, combiners, faults, re-entrancy,
the cycle cap
AttributeSetSnapshotTests.cs 10 cases — base-only capture, rewind restore
Tests/EditMode/Integrations/AttributesCrafting/
AttributeLevelSourceTests.cs 9 cases — the mapping, the fallback, late binding,
and Crafting's own LevelGateValidator gating
Tests/PlayMode/Integrations/SaveAttributes/
AttributesSaveParticipantPlayModeTests.cs
13 cases — round trips, the rewind, identity,
versioning, the refusal taxonomy
Three assemblies: RevFramework.Attributes.Tests.EditMode, RevFramework.Integrations.Attributes.Crafting.Tests.EditMode and RevFramework.Integrations.Save.Attributes.Tests.PlayMode — each constrained on the defines of the systems it needs, so a deleted system takes its tests with it.
Those per-file counts are a snapshot, not a gate — nothing in CI asserts them, so treat a small disagreement as this page being behind rather than as a missing test.
Why the core is EditMode, and why that is not a shortcut¶
AttributeSet creates its state at the head of every public member rather than in Awake — a design decision the tests then get to lean on. EditMode never runs Awake, which is precisely the trap: a component that caches its dependencies there turns an EditMode suite vacuous, passing against an inert object. Here the first value test asserts an authored row is readable with no lifecycle having run, so the suite cannot go vacuous without that test going red first.
What genuinely needs Play Mode has it: the save participant resolves owners through StableIdOwners' scene walk, so its round trips run there, in the shape the other save participants' fixtures established — capture, mutate, restore, then assert both that the values came back and that nothing else moved.
No internals were needed, and none are exposed¶
Every core test drives the public surface: authored rows go in through the serialized field (the inspector is the supported author, so the tests author the same way, with a renamed-field guard), combiners through SetCombiner, contributions through real provider components. The core assembly therefore declares no friend assemblies at all — where Loot needs an InternalsVisibleTo for states its public factory cannot build, Attributes has no such state, and the cleanest proof is that nothing asked for the keys.
The one friend that exists is the integration test assembly for the Crafting adapter, because the adapter component is internal by the same rule as every shipped adapter: it is a thing you add in the inspector, not API.
The formulas in the tests are the tests' own¶
The combiner fixtures ship a summing combiner, a constant combiner and a derived-stat combiner — written inside the test assembly, because that is where the framework says formulas live: with the project. The suite proves contributions arrive, faults are isolated and bounds clamp afterwards; it deliberately never asserts what "correct stacking" is, because the system under test refuses to know.
Negative controls were run against production, not assumed
Five deliberate breaks — the event raise disabled, clamping disabled, the component's restore made inert, the participant's restore made inert, and nested reads forced back onto shared buffers — were each applied to the production code, confirmed to turn the right test families red and nothing else, and reverted. The re-entrancy control matters most: the first version of that test passed against the very breakage it named, and only earned its place after being rewritten to fail. A guard that has never failed is a guess about a guard.