π Lesson 5.2: Debugging and the Unity Test Framework
Fast code (Lesson 5.1) still has to be correct. This lesson covers the two halves of reliability: debugging β finding and fixing bugs with logging, Gizmos, and breakpoints β and testing β writing automated checks with the Unity Test Framework so bugs stay fixed as your project grows.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Use
Debug.Log/LogWarning/LogErroreffectively (with context objects) - Visualize logic in the Scene view with
Debug.DrawRayand Gizmos - Attach a debugger and use breakpoints to inspect runtime state
- Diagnose the classic Unity bugs (null/missing references, execution order)
- Write Edit Mode and Play Mode tests with the Unity Test Framework
- Structure code so gameplay logic is testable, separate from MonoBehaviours
Estimated Time: 60 minutes
Project: Debug a broken component with logging and Gizmos, then write unit tests for a pure-C# scoring rule.
In This Lesson
Debugging Mindset
Debugging is a science, not luck: form a hypothesis about what's wrong, gather evidence, and narrow it down. Unity gives you tools for each step β the Console for messages, Gizmos for seeing invisible logic, and a debugger for freezing time and inspecting state.
what could cause it?"] H --> E["Gather evidence
(log, gizmo, breakpoint)"] E --> N["Narrow down
the cause"] N --> F["Fix & verify"] F -->|"still wrong?"| H style E fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style F fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px
π‘ The most common Unity bug isn't in your logic β it's a reference that's null, an event function that's misspelled (Lesson 1.2), or code running in the wrong order. Before suspecting complex logic, check the simple things: Is the reference assigned in the Inspector? Is the object active? Did Awake run before this?
Logging Well
The Debug class writes to the Console. Three severity levels help you scan output β and errors/warnings can be filtered separately:
Debug.Log("Player spawned"); // white β info
Debug.LogWarning("Health below 10%"); // yellow β attention
Debug.LogError("Missing weapon reference!"); // red β a real problem
// Pass 'this' (or any Object) as a second argument: clicking the message
// pings that object in the Hierarchy β invaluable for "which one broke?".
Debug.LogError("Target not assigned", this);
β Make logs useful, then remove them
Include values, not just labels: Debug.Log($"Health: {current}/{max}") beats Debug.Log("took damage"). Log state at decision points to see what the code actually saw. But logging every frame floods the Console and even allocates strings (Lesson 5.1) β remove or guard temporary logs when you're done. Wrap verbose logging in [Conditional("DEBUG")] methods or an if (verbose) flag so it's off in release.
β οΈ Read the whole error, including the stack trace
A NullReferenceException in the Console expands to a stack trace β the chain of calls that led to it. Click it: it shows the exact file and line. Beginners often panic at the red text; the fix is usually right there in the first line of the trace ("object reference not set" + the line that dereferenced null).
Visual Debugging: Gizmos
Some bugs are spatial β a detection radius that's wrong, a ray pointing the wrong way, a patrol path off by a bit. Numbers in the Console won't reveal these; drawing them will. Unity offers two visual tools:
| Tool | Where | Use for |
|---|---|---|
Debug.DrawRay / DrawLine | Scene view (and Game view if Gizmos on) | Quick rays/lines from inside Update |
OnDrawGizmos() | Scene view, always (even when not playing) | Persistent shapes β radii, bounds, waypoints |
using UnityEngine;
public class EnemyVision : MonoBehaviour
{
[SerializeField] private float viewDistance = 10f;
[SerializeField] private float detectRadius = 3f;
void Update()
{
// Draw the forward view ray each frame (red), visible in the Scene view.
Debug.DrawRay(transform.position, transform.forward * viewDistance, Color.red);
}
// Gizmos draw in the editor even when not playing β great for tuning.
void OnDrawGizmosSelected()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, detectRadius); // see the detection range
}
}
π‘ Gizmos make invisible logic visible
A DrawWireSphere at your detection radius instantly shows if it's too big or too small β no guessing at numbers. OnDrawGizmos runs always; OnDrawGizmosSelected only when the object is selected (less clutter). These calls are editor-only and cost nothing in a build. When a spatial thing "doesn't work," draw it before you theorize.
Breakpoints & Common Bugs
Logging shows values after the fact; a breakpoint freezes the game at a line so you can inspect everything β variable values, the call stack, and step through line by line. Attach your IDE (Visual Studio / Rider) to Unity ("Attach to Unity"/"Play"), set a breakpoint by clicking the gutter, and trigger the code.
β Breakpoint vs Debug.Log
Use logging for a quick trace or something that happens fast/often. Use a breakpoint when you need to poke around a specific moment β inspect several variables, check the call stack ("who called this?"), or step through branching logic. Breakpoints don't require editing and re-running code, but they pause the whole editor.
Most Unity bugs fall into a few recognizable buckets. Knowing the pattern speeds the fix:
| Symptom | Likely cause | Check |
|---|---|---|
NullReferenceException | Reference never assigned, or lookup failed | Inspector slot filled? GetComponent found it? (Lesson 1.3) |
MissingReferenceException | The object was destroyed but still referenced | Guard with != null; unsubscribe events (Lesson 3.2) |
| Event function never runs | Typo / wrong case / wrong signature | Exact name & params (Lesson 1.2); is the object active? |
| Value is wrong on frame 1 | Read in Start before another Awake | Two-phase startup order (Lesson 1.2) |
| Works in Editor, breaks in build | Editor-only API, or a missing scene/asset | Scene in Build Settings? (Lesson 4.1) |
The Unity Test Framework
Debugging fixes a bug once. A test makes sure it stays fixed. The Unity Test Framework (UTF) builds on NUnit β the same framework from the Intermediate C# course β and runs from Window β General β Test Runner. There are two kinds:
run without Play mode
(fast β pure logic)"] UTF --> PM["Play Mode tests
run in a live scene
(MonoBehaviours, physics, frames)"] style EM fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px style PM fill:#eff6ff,stroke:#3b82f6,stroke-width:2px
An Edit Mode test is a plain NUnit test β perfect for pure C# logic. It follows the ArrangeβActβAssert pattern you learned before:
using NUnit.Framework;
public class ScoreRulesTests
{
[Test]
public void ComboMultiplier_DoublesScore_AtThreeHits()
{
// Arrange
var rules = new ScoreRules();
// Act
int score = rules.Points(baseValue: 100, comboHits: 3);
// Assert
Assert.AreEqual(200, score);
}
}
A Play Mode test runs in a real scene and can span frames using [UnityTest] with an IEnumerator (the coroutine shape from Lesson 3.3) β so you can yield and check results after time passes:
using System.Collections;
using UnityEngine;
using UnityEngine.TestTools; // [UnityTest]
using NUnit.Framework;
public class MovementTests
{
[UnityTest]
public IEnumerator Player_MovesForward_OverTime()
{
// Arrange: build a live object with the component under test.
var go = new GameObject();
var mover = go.AddComponent<PlayerMover>();
Vector3 start = go.transform.position;
// Act: let a few frames pass.
yield return new WaitForSeconds(0.5f);
// Assert: it actually moved.
Assert.Greater(go.transform.position.z, start.z);
}
}
π‘ Edit Mode vs Play Mode β pick the cheaper one
Prefer Edit Mode tests: they're fast, don't start Play mode, and are ideal for logic (scoring, inventory, state rules). Use Play Mode only when you truly need the running engine β physics, coroutines, real component interaction. Tests live in a special test assembly (the Test Runner can create the folder/asmdef for you).
Writing Testable Code
Here's the payoff of good architecture: logic that doesn't depend on Unity is trivial to test. A scoring rule buried inside a MonoBehaviour needs a running scene to test; the same rule in a plain C# class can be tested in milliseconds, in Edit Mode.
// β Hard to test: logic tangled with MonoBehaviour + Unity state.
public class ScoreManager : MonoBehaviour
{
private int score;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
score += 100 * (comboHits >= 3 ? 2 : 1); // rule hidden in Update
}
}
// β
Easy to test: the RULE is a pure class; the MonoBehaviour just calls it.
public class ScoreRules // no MonoBehaviour β plain C#, unit-testable
{
public int Points(int baseValue, int comboHits)
=> baseValue * (comboHits >= 3 ? 2 : 1);
}
public class ScoreManager : MonoBehaviour
{
private readonly ScoreRules rules = new();
private int score;
public void RegisterHit(int comboHits) => score += rules.Points(100, comboHits);
}
β Separate logic from the engine
This echoes the ScriptableObject lesson's "data vs behavior" split and 5.1's testable-code note: keep decisions and rules in plain C# classes, and let MonoBehaviours handle Unity glue (input, references, lifecycle). You get code that's easier to test, reuse, and reason about β and Edit Mode tests that run instantly.
π‘ What to test first: your logic β scoring, inventory limits, damage formulas, save/load round-trips, state-machine transitions (Lesson 4.1). These are where subtle bugs hide and where a fast Edit Mode test pays off every time you refactor. You don't need 100% coverage; test the rules that would hurt if they broke.
Exercise & Quiz
ποΈ Exercise: Debug, then Test
Objective: Practice both halves β visualize a bug with Gizmos, then lock a rule down with a unit test.
Instructions:
- Add the
EnemyVisionscript (Section 3) to an object. UseOnDrawGizmosSelectedto draw its detection radius andDebug.DrawRayfor its view direction; tune the values by seeing them in the Scene view. - Extract a pure-C#
ScoreRulesclass withint Points(int baseValue, int comboHits)that doubles the score at 3+ combo hits. - Open Window β General β Test Runner, create an Edit Mode test assembly, and write tests for: 1 hit (no bonus), 3 hits (doubled), and 5 hits (doubled).
- Run the tests β all green. Then change the rule to a bug (e.g.
> 3instead of>= 3) and watch the 3-hit test go red, proving the test catches it. - Fix it back to green. You now have a rule that can't silently break again.
Starter Code:
public class ScoreRules
{
// TODO: double the base value when comboHits is 3 or more.
public int Points(int baseValue, int comboHits) => baseValue;
}
using NUnit.Framework;
public class ScoreRulesTests
{
// TODO: [Test] methods for 1 hit, 3 hits, 5 hits using Assert.AreEqual.
}
π‘ Hint
Rule: baseValue * (comboHits >= 3 ? 2 : 1). Each test: Arrange var rules = new ScoreRules();, Act int result = rules.Points(100, hits);, Assert Assert.AreEqual(expected, result);. Expected: 1 hit β 100, 3 hits β 200, 5 hits β 200.
β Solution
public class ScoreRules
{
public int Points(int baseValue, int comboHits)
=> baseValue * (comboHits >= 3 ? 2 : 1);
}
using NUnit.Framework;
public class ScoreRulesTests
{
[Test]
public void OneHit_NoBonus()
{
var rules = new ScoreRules();
Assert.AreEqual(100, rules.Points(100, 1));
}
[Test]
public void ThreeHits_Doubled()
{
var rules = new ScoreRules();
Assert.AreEqual(200, rules.Points(100, 3));
}
[Test]
public void FiveHits_Doubled()
{
var rules = new ScoreRules();
Assert.AreEqual(200, rules.Points(100, 5));
}
}
Because ScoreRules is plain C#, these are fast Edit Mode tests β no Play mode, no scene. Break the rule and a test turns red immediately; that's a regression caught before it ever reaches the game.
π― Quick Quiz
Question 1: What's the advantage of passing this to Debug.LogError("...", this)?
Question 2: You want to test a pure scoring formula quickly, without starting the game. Which test type fits?
Question 3: Why extract a scoring rule into a plain C# class instead of leaving it in a MonoBehaviour's Update?
Summary
π Key Takeaways
- Debug scientifically: observe β hypothesize β gather evidence β narrow β fix. Suspect the simple causes (null refs, typos, order) first.
Debug.Log/LogWarning/LogErrorβ include values, passthisas context to ping the object, and read the full stack trace. Remove noisy temporary logs.- Use Gizmos (
OnDrawGizmos) andDebug.DrawRayto make spatial logic visible; use breakpoints to freeze and inspect runtime state. - Know the classic Unity bug patterns:
NullReference/MissingReference, misspelled event functions, execution-order surprises, Editor-vs-build differences. - The Unity Test Framework (NUnit-based): Edit Mode tests for fast pure logic, Play Mode (
[UnityTest]) for engine behavior across frames. Follow ArrangeβActβAssert. - Keep logic in plain C# classes, MonoBehaviours as glue β testable, reusable, and easy to reason about.
π Additional Resources
- Scripting Reference β Debug (Log, DrawRayβ¦)
- Scripting Reference β OnDrawGizmos
- Manual β Unity Test Framework
- Manual β Debugging C# code (breakpoints)
π What's Next?
You can build fast, correct, well-tested Unity C#. Time to put it all together. Lesson 5.3, the Capstone Project, guides you through building a complete gameplay system β components, lifecycle, input, physics, data, events, managers, UI, save/load, and performance β everything from this course in one cohesive build.
π Reliable and verified!
You can now hunt down any bug and write tests that keep it dead. That confidence is what lets you build big without fear of breaking what works.