There is nothing more gaslighting than a test that passes on Monday and fails on Tuesday, without you changing a single line of code.
Today, I’ve been diving deep into Unity integration testing for my Selection System. If you read my last post On the Illusion of Architectural Choice in Unity, you know I decided to lean into a ScriptableObject-based architecture. It felt elegant. It felt decoupled.
Then I tried to test it. And that’s when I realized that while ScriptableObjects are great for architecture, they can be absolute nightmare for testing if you don't know the "hidden" rules of it.
The "Shared State" Trap (Again)
Remember how I mentioned in my last article that SOs are assets and their state persists? Well, that architectural quirk becomes a lethal bug when writing tests.
I wrote a test: Select a unit → Verify it's selected. Pass.
I wrote a second test: Deselect all units → Verify nothing is selected. Pass.
But run them in a different order and they started failing. Why? Because the context was dirty.
Loading a fresh scene before each test should have given me a clean slate, but the unit from Test A was still selected when Test B started. (Turns out scene loading had its own secrets, but that's a problem for later in this post).
The Lesson: Even in PlayMode tests, ScriptableObjects do not magically wipe themselves clean between tests. You have to manually reset your runtime state using the attribute [UnitySetUp].
[UnitySetUp]
public IEnumerator SetUp()
{
...
ClearSelection(); // Clearing the list containing the selected units
}
The Frame I Forgot to Wait For
Once I fixed the SO state, I hit a second, sneakier problem: I thought I was loading a clean scene before every test. I wasn't.
My first attempt was simple: call SceneManager.LoadScene(scenePath) in my setup, then immediately go find the GameObject I needed. But it was null.
I stared at that NullReferenceException for way longer than I'd like to admit. The object was right there in the scene. I could see it in the Editor. Why couldn't my code find it?
The answer was buried in a line of the Unity docs: LoadScene is semi-asynchronous. Calling it doesn't load the scene right away. It queues the load for the next frame. My setup code wasn't waiting for that frame.
The Fix: Stop treating scene loading as instant and actually wait for it to finish:
[UnitySetUp]
public IEnumerator SetUp()
{
yield return LoadTestScene();
ClearSelection();
}
private static IEnumerator LoadTestScene()
{
var loadOperation = EditorSceneManager.LoadSceneAsyncInPlayMode(
ScenePath,
new LoadSceneParameters(LoadSceneMode.Single));
while (!loadOperation.isDone)
{
yield return null;
}
yield return null; // one extra frame for safety
}
Was it worth the pain?
Here's the thing: none of this was new to me conceptually. I've spent 7-8 years writing Java/Spring Boot integration tests, so "isolate your state" and "don't trust a dirty context" weren't foreign ideas. I saw these problems coming before I'd even written my first test class. After all, that's just what integration testing demands, regardless of framework.
What I wasn't prepared for was how Unity-specific the actual mechanics turned out to be. But that's the price of admission: you learn the framework's quirks by tripping over them. And it was worth it.
Finally, I have a robust suite of tests for my Selection System, and I can sell the asset knowing that any future change won't quietly break it.
What about you? Do you actually write integration tests in Unity, or do you rely on the "Play button and pray" method? Let me know in the comments!
My Game's Links:
The Weight of One - Official Channel
The Weight of One - Itch.io Page
My Socials:
My Linkedin
My Personal Channel
You can find my Dev Vlogs in both Youtube channels!
Top comments (0)