Effortless regression testing for Unity: capture, inspect, and verify complex UI hierarchies, components, and scene states against deterministic baselines.
As Unity projects grow in complexity, writing assertions for deep UI hierarchies, component states, animations, physics setups, and level structures becomes tedious and fragile.
Unity State Snapshot captures complex Unity object graphs and game states into structured .verified.json snapshots. On subsequent test runs, the engine extracts the live state, compares it against the verified baseline, and highlights exact differences with Git-style unified diffs.
[Test]
public void MainMenu_InitialState_MatchesSnapshot()
{
var menuGo = Object.Instantiate(menuPrefab);
// One-line snapshot verification:
Snapshot.Verify(menuGo);
}- π― Explicit
SnapshotStatic API: Simple, non-intrusive static entry points (Snapshot.VerifyandSnapshot.Compare) forGameObject,Component,Scene,IEnumerable<GameObject>, and custom DTOs. - π Resolution-Agnostic Normalization: Automatically normalizes
ScreenSpaceOverlayCanvases and dynamic viewport dimensions (pixelRect,renderingDisplaySize), ensuring tests produce 100% byte-identical baselines in the Unity Editor, Play Mode, and headless CI batchmode (640x480). - π§© 35+ Built-in Component Handlers: Handlers with safe, non-leaking property extraction across UI (uGUI, TextMeshPro, UI Toolkit), 2D/3D Physics, 2D/3D Rendering, Audio, Animation, Navigation, Tilemaps, and Video.
- π Rich Unified Line Diffs: Failures output Git-style unified diffs (
--- Expected/+++ Received) embedded directly in the test exception, alongside the file paths for easy review. - β‘ AutoVerify Workflow: Auto-accept new baselines seamlessly via
settings.AutoVerify(true)or theSNAPSHOT_AUTO_VERIFY=1environment variable. - π‘οΈ Non-Throwing
Snapshot.CompareAPI: Inspect diffs and comparison results programmatically without catching exceptions. - π§Ή Deterministic Scrubber Pipeline: Normalizes IEEE 754 negative zero (
-0.0f->0.0f), strips runtime(Clone)and(Instance)suffixes, normalizes vector/color precisions, and sorts all JSON keys alphabetically. - π Safe Reflection Engine: Automatically extracts public fields and
[SerializeField]private fields while skipping base Unity engine properties (destroyCancellationToken,didAwake) and memory-leaking accessors (renderer.material,meshFilter.mesh).
- Open the Unity Package Manager (
Window>Package Manager). - Click
+and select Add package from git URL... - Enter:
https://github.com/pereviader/UnityStateSnapshot.git?path=Packages/com.pereviader.unitystatessnapshot
Add the package to your Packages/manifest.json:
{
"dependencies": {
"com.pereviader.unitystatessnapshot": "https://github.com/pereviader/UnityStateSnapshot.git?path=Packages/com.pereviader.unitystatessnapshot"
}
}Call Snapshot.Verify(...) inside your NUnit EditMode or PlayMode test. The snapshot file will be automatically created in a Snapshots~ subfolder next to your test file:
using NUnit.Framework;
using UnityEngine;
using PereViader.UnityStateSnapshot;
[TestFixture]
public class InventoryTests
{
[Test]
public void InventoryView_OpenedState_MatchesSnapshot()
{
var inventoryGo = Object.Instantiate(inventoryPrefab);
// Asserts state matches InventoryTests.InventoryView_OpenedState.verified.json
Snapshot.Verify(inventoryGo);
}
}Configure float precision, custom directories, or specific member filters:
[Test]
public void SettingsDialog_WithCustomSettings()
{
var settings = new VerifySettings()
.WithFloatPrecision(2) // Round floats to 2 decimal places
.IncludeDisabledObjects(true) // Capture inactive child GameObjects
.IncludeSerializedFields(true) // Capture private [SerializeField] fields
.UseDirectory("CustomSnapshots") // Save snapshots in a custom folder
.IgnoreComponent<AudioSource>() // Exclude entire component types
.IgnoreMember("LivePingMs"); // Exclude dynamic property names
Snapshot.Verify(settingsDialog, "SettingsDialog_CustomState", settings);
}// 1. Single Component
Snapshot.Verify(myTextMeshProComponent);
// 2. Multiple GameObjects / Root hierarchies
Snapshot.Verify(new[] { playerRoot, uiCanvasRoot });
// 3. Full Unity Scene
Snapshot.Verify(UnityEngine.SceneManagement.SceneManager.GetActiveScene());
// 4. Extract state DTO without asserting
GameObjectSnapshotDTO dto = Snapshot.Extract(myGameObject, settings);Inspect differences programmatically without raising test exceptions:
SnapshotComparisonResult result = Snapshot.Compare(playerObject);
if (!result.IsMatch)
{
Debug.LogWarning($"Snapshot mismatch for {result.SnapshotName}:\n{result.Diff}");
Debug.Log($"Expected baseline at: {result.VerifiedFilePath}");
Debug.Log($"Received output at: {result.ReceivedFilePath}");
}When intentionally changing UI or gameplay logic, update the verified baselines automatically:
var settings = new VerifySettings().AutoVerify(true);
Snapshot.Verify(myGameObject, settings);SNAPSHOT_AUTO_VERIFY=1 unitycli.sh test --editmodeDecorate non-deterministic or transient members with [SnapshotIgnore]:
public class PlayerState : MonoBehaviour
{
public int Level = 10; // Captured
[SerializeField]
private int _health = 100; // Captured
[SnapshotIgnore]
public string SessionToken; // Ignored
[SnapshotIgnore]
public float LiveFps => 120.5f; // Ignored
}Extend the snapshot engine with custom extractors:
public class HealthHandler : ISnapshotComponentHandler
{
public int Priority => 100; // Higher priority runs before default reflection
public bool CanHandle(Type componentType)
{
return typeof(HealthComponent).IsAssignableFrom(componentType);
}
public void Extract(Component component, IDictionary<string, object> targetState, VerifySettings settings)
{
var health = (HealthComponent)component;
targetState["Health"] = $"{health.CurrentHP}/{health.MaxHP}";
targetState["IsAlive"] = health.IsAlive;
}
}
// Register globally or per-test:
var settings = new VerifySettings().RegisterComponentHandler(new HealthHandler());
Snapshot.Verify(playerGo, settings);| Subsystem | Built-in Handlers |
|---|---|
| UI Toolkit | UIDocument (with recursive VisualElement tree extraction, classes, layout, and values) |
| uGUI Controls | Button, Toggle, Slider, Scrollbar, ScrollRect, Selectable |
| uGUI Graphics & Layout | Image, RawImage, Text, CanvasGroup, HorizontalLayoutGroup, VerticalLayoutGroup, GridLayoutGroup, ContentSizeFitter, AspectRatioFitter |
| Canvas | Canvas (with resolution-independent ScreenSpaceOverlay normalization), CanvasScaler |
| TextMeshPro | TextMeshProUGUI, TMP_Text, TMP_InputField, TMP_Dropdown |
| Physics 3D | Rigidbody, CharacterController, BoxCollider, SphereCollider, CapsuleCollider, MeshCollider |
| Physics 2D | Rigidbody2D, BoxCollider2D, CircleCollider2D, CapsuleCollider2D, PolygonCollider2D, TilemapCollider2D |
| 2D Tilemaps & Grids | Tilemap, Grid, TilemapRenderer |
| AI & Navigation | NavMeshAgent, NavMeshObstacle, OffMeshLink |
| Rendering & Cameras | SpriteRenderer, MeshFilter, MeshRenderer, SkinnedMeshRenderer, Camera, Light, LineRenderer, TrailRenderer, ParticleSystemRenderer |
| Animation & Audio | Animator (with active clips and parameters), Animation, ParticleSystem, AudioSource, AudioListener |
| Video | VideoPlayer |
| Custom Scripts | MonoBehaviour (deep reflection for public fields, properties, [SerializeField] private fields, collections, and nested objects) |
When a test detects a difference, SnapshotMismatchException displays a Git-style diff:
SnapshotMismatchException: Snapshot mismatch for 'SettingsScreen_OpenedState'
--- Expected: Assets/MockApp/Tests/Snapshots~/SettingsScreen_OpenedState.verified.json
+++ Received: Assets/MockApp/Tests/Snapshots~/SettingsScreen_OpenedState.received.json
@@ -14,7 +14,7 @@
"Components": [
{
"TypeName": "Slider",
- "Value": "50"
+ "Value": "75"
}
]
MIT License. See LICENSE for details.