# 2. Edit Mode Tests and NUnit Part of [[Unity Test Framework]]. Previous: [[1. Test Assemblies and the Test Runner]]. Next: [[3. UnityTest Async Tests and Domain Reloads]]. > [!note] Verification status > Attributes and assertions below are standard NUnit 3 / Unity Test Framework APIs (`[Test]`, `[SetUp]`, `[TearDown]`, `[OneTimeSetUp]`, `[OneTimeTearDown]`, `[TestCase]`, `[Values]`, `[Category]`, `[Explicit]`, `[Ignore]`, `[Timeout]`, `[MaxTime]`, `LogAssert`, `[TestMustExpectAllLogs]`, `[UnityPlatform]`, `[ConditionalIgnore]`, `[ParametrizedIgnore]`). Checked against the Test Framework 1.7 API and changelog on 2026-11-10. Unity ships a custom NUnit fork via `com.unity.ext.nunit`; the exact fork version in a given 6000.5 Editor is **unverified**. **Code not compiled in Unity.** ## 2.1 A first test Put this in your Edit Mode test assembly, next to (or referencing) a tiny production type. The production type can live in `Assets/Inventory/Runtime/`: ```csharp public sealed class IntBag { readonly int _capacity; readonly System.Collections.Generic.List<string> _items = new(); public IntBag(int capacity) { if (capacity < 0) throw new System.ArgumentOutOfRangeException(nameof(capacity)); _capacity = capacity; } public int Count => _items.Count; public int Capacity => _capacity; public bool IsFull => _items.Count >= _capacity; public bool TryAdd(string id) { if (string.IsNullOrEmpty(id) || IsFull) return false; _items.Add(id); return true; } public bool Remove(string id) => _items.Remove(id); } ``` The test: ```csharp using NUnit.Framework; public class IntBagTests { [Test] public void TryAdd_WhenEmpty_IncreasesCount() { var bag = new IntBag(3); bool added = bag.TryAdd("herb"); Assert.IsTrue(added); Assert.AreEqual(1, bag.Count); } [Test] public void TryAdd_WhenFull_ReturnsFalseAndLeavesCount() { var bag = new IntBag(1); bag.TryAdd("herb"); bool added = bag.TryAdd("gem"); Assert.IsFalse(added); Assert.AreEqual(1, bag.Count); } [Test] public void Constructor_RejectsNegativeCapacity() { Assert.Throws<System.ArgumentOutOfRangeException>(() => new IntBag(-1)); } } ``` Open the Test Runner, select the fixture, click **Run**. Green means the assertions held. Red gives you the failed assertion and a stack trace you can click. ## 2.2 Assertions you will actually use NUnit has a classic API (`Assert.AreEqual`) and a constraint API (`Assert.That(x, Is.EqualTo(y))`). Both work. Pick one style for the class and stick to it. The classic API is easier to read aloud in a lecture. | Assertion | Meaning | |---|---| | `Assert.AreEqual(expected, actual)` | Equality, including collections with the collection overloads | | `Assert.AreNotEqual` | Inequality | | `Assert.IsTrue` / `Assert.IsFalse` | Booleans; prefer a named method so the assertion reads as a sentence | | `Assert.IsNull` / `Assert.IsNotNull` | References | | `Assert.AreSame` / `Assert.AreNotSame` | Reference identity, not value | | `Assert.Throws<T>(code)` | The delegate throws `T` | | `Assert.DoesNotThrow(code)` | The delegate does not throw | | `Assert.That(value, Is.InRange(a, b))` | Constraint form, useful for floats | | `Assert.AreEqual(expected, actual, delta)` | Floats/doubles within `delta` | For floats (`health`, `speed`), **always** use a delta. `0.1f + 0.2f` is not `0.3f`. ```csharp [Test] public void JumpHeight_MatchesMetricsWithinAMillimetre() { float height = 1.5f * 1.5f / (2f * 9.81f); // v^2 / 2g, example only Assert.AreEqual(0.11468f, height, 0.0001f); } ``` ## 2.3 SetUp and TearDown `[SetUp]` runs before every test in the fixture. `[TearDown]` runs after, even when the test failed. `[OneTimeSetUp]` / `[OneTimeTearDown]` run once per fixture. ```csharp using NUnit.Framework; public class IntBagTests { IntBag _bag; [SetUp] public void SetUp() { _bag = new IntBag(3); } [TearDown] public void TearDown() { _bag = null; } [Test] public void NewBag_IsEmpty() { Assert.AreEqual(0, _bag.Count); Assert.IsFalse(_bag.IsFull); } } ``` Use `[SetUp]` for a clean, cheap starting state. Do not hide the interesting arrange step there — if the test is "given a full bag," arrange the full bag in the test so the reader can see it. `[TearDown]` is for cleanup that would leak (temp files, objects added to a scene, statics you mutated). Prefer not to mutate statics at all. ## 2.4 Parameterized tests When the same rule has many examples, do not copy the test body. ```csharp using NUnit.Framework; public class IntBagCapacityTests { [TestCase(0)] [TestCase(1)] [TestCase(8)] public void Constructor_AcceptsNonNegativeCapacity(int capacity) { var bag = new IntBag(capacity); Assert.AreEqual(capacity, bag.Capacity); } [Test] public void TryAdd_FailsForNullOrEmpty( [Values(null, "")] string id) { var bag = new IntBag(3); Assert.IsFalse(bag.TryAdd(id)); Assert.AreEqual(0, bag.Count); } } ``` `[TestCaseSource]` (a static field or method that returns rows) is the next step when each row needs a name or more than a few arguments. `[Values]` on a parameter expands into one test per value. Unity also ships `PreservedValuesAttribute` as a stripping-friendly stand-in for `[Values]` when you run tests in a Player with a high managed stripping level. `[ParametrizedIgnore]` can ignore a test only for some argument combinations. ## 2.5 Categories, ignore, and platforms ```csharp using NUnit.Framework; using UnityEngine.TestTools; using UnityEngine; [Category("Inventory")] public class IntBagSmokeTests { [Test] [Explicit("Long; run when you change bag capacity rules")] public void FillingAHugeBag_DoesNotThrow() { var bag = new IntBag(10000); for (int i = 0; i < 10000; i++) Assert.IsTrue(bag.TryAdd("x" + i)); } [Test] [UnityPlatform(RuntimePlatform.WindowsEditor, RuntimePlatform.OSXEditor, RuntimePlatform.LinuxEditor)] public void EditorOnly_Sanity() { Assert.IsTrue(true); } } ``` - `[Category("Inventory")]` lets you run a subset from the Test Runner or with `-testCategory` on the command line (part 6). - `[Explicit]` tests do not run as part of "run all" unless you select them (or filter to them). Use them for slow or manual-ish cases. - `[Ignore("reason")]` skips the test and shows the reason in the UI. - `[UnityPlatform(...)]` restricts a test to listed `RuntimePlatform` values. `[ConditionalIgnore]` is the hook for your own named conditions (CI, a package missing, and so on). ## 2.6 Logs are part of the contract Unity Test Framework fails a test if it logs an **error** that you did not expect. `Debug.LogWarning` does not fail a test by default. ```csharp using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; public class HealthTests { [Test] public void Damage_WhenDead_LogsError() { LogAssert.Expect(LogType.Error, "Damage applied to a dead actor."); // production code that logs that exact message } } ``` `LogAssert.Expect` has overloads that match a message, a regex, or (from Test Framework 1.4.0) any severity. If a test must treat any unexpected log as a failure, put `[TestMustExpectAllLogs]` on the method or class — it is equivalent to calling `LogAssert.NoUnexpectedReceived()` at the end. This is how you test "the save system reports a missing file" without failing the run for the `Debug.LogError` that production code uses on purpose. ## 2.7 What belongs in Edit Mode Do test: - Math, ranges, clamps, XP curves ([[3.Leveling and XP Curves]]), damage formulas ([[5.Damage Formulas]]) - Inventory capacity, stacking, quest flag logic ([[Inventory Systems]], [[Quest Systems]]) - Serialization round-trips of your own data types ([[High Scores, Saves and Player Stats]]) - ScriptableObject instances you construct with `ScriptableObject.CreateInstance<T>()` and destroy in teardown Do not test in Edit Mode (move to Play Mode): - `Instantiate` of a prefab that needs `Awake`/`Start` to finish - Physics contacts, triggers, `CharacterController.Move` - Anything that requires a loaded scene or the Audio mixer graph A good class rule: if the method could run correctly in a console app with no Unity scene, it is an Edit Mode test. ## Official docs - Create a test assembly: https://docs.unity.com/en-us/engine/6000.5/manual/scripting/test-framework-introduction/getting-started/workflow-create-test-assembly - Test Framework 1.7 API: https://docs.unity3d.com/Packages/[email protected]/api/UnityEngine.TestTools.html - Test Framework 1.7 changelog (`[ParametrizedIgnore]`, `LogAssert.Expect` overloads, `[TestMustExpectAllLogs]`): https://docs.unity3d.com/Packages/[email protected]/changelog/CHANGELOG.html