Skip to content

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.