# 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