unity-tests-without-asmdef
Add NUnit EditMode tests to a Unity project whose first-party code has no assembly definitions (everything compiles into Assembly-CSharp) - where the tests live, what compiles, how to test pure logic,
SKILL.mdskills/unity-tests-without-asmdef/SKILL.md
GitHubEditMode tests without assembly definitions
Where tests go
Assets/<Project>/Tests/Editor/*.cscompiles into Assembly-CSharp-Editor, which referencesnunit.framework,UnityEditor.TestRunnerandUnityEngine.TestRunnerout of the box. It can see every type in Assembly-CSharp and in auto-referenced packages.- PlayMode tests in the predefined assemblies need
playModeTestRunnerEnabled: 1inProjectSettings/ProjectSettings.asset("Enable playmode tests for all assemblies"); use them only when the code truly needs the player loop. - Namespace them (
<Project>.Tests) and keep the folder nameTestsso the migration to real test assemblies later is a move plus an asmdef.
What to test first
- Pure logic: state machines, mappers, parsers, calculators. Construct, feed inputs, assert outputs; observe transitions through events rather than private fields.
- Static guards and domain-reload resets: read the private static through reflection
(
typeof(T).GetField("isLoading", BindingFlags.Static | BindingFlags.NonPublic)), invoke the[RuntimeInitializeOnLoadMethod]reset through reflection, assert the public property. - In-memory fakes of service interfaces (an
ICloudManagerthat stores objects): defaults, storage semantics, listener behaviour, no-op writes. - MonoBehaviours outside Play Mode:
new GameObject().AddComponent<T>()works in EditMode tests, but Awake, Start and the serializer do not run, so serialized fields (includingUnityEventfields) can be null; initialise them in the test andObject.DestroyImmediatethe host in[TearDown]. Coroutines andSceneManager.LoadSceneAsyncare Play Mode only.
Pitfalls seen in practice
- State machines often fire the initial state's "enter" event on the first update; prime the machine with one neutral sample before attaching recorders.
- In-place mutation of objects returned by a getter defeats "did it change" comparisons against the stored instance; compare against a remembered value.
- Time-dependent logic that uses
Stopwatch: sleep past the window (Thread.Sleep(150)for an 80 ms trigger); keep the total suite under a few seconds. Debug.Login production code is fine in tests;Debug.LogErrorfails the test unlessLogAssert.Expect.
Run
unity test <project> --mode EditMode --report-format junit --output TestResults/editmode.junit.xml --timeout 900 --no-banner
The project must be closed (the CLI spawns its own editor). Exit code 0 means every test passed; the JUnit
file lists each testcase with failures. From a live editor, the Meta XR bridge Test Runner tools or the Test
Runner window run the same tests. Ignore TestResults/ in git and attach the report to the review as evidence.