uw-unity-test-runner-7a5df1 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited uw-unity-test-runner-7a5df1 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Generate NUnit tests mapped from GDD Gherkin scenarios.
docs/GDD.md for Gherkin scenarios (Given/When/Then) to convert into tests.Tests.asmdef exists for the feature — create with uw-unity-feature-scaffold if needed.docs/CODING_STANDARDS.md for async patterns (Awaitable + CancellationToken) used in PlayMode tests.docs/NAMING_CONVENTIONS.md for test class and file naming.docs/ProjectConfig.yaml -> mcp.unity_mcp — if true, use run_tests / get_test_job to execute tests. Otherwise, instruct the user to run via Unity Editor (Window -> General -> Test Runner).| Type | Location | When to Use | Runner |
|---|---|---|---|
| EditMode | Tests/EditMode/ | Pure logic — no MonoBehaviour lifecycle, no scene required | [Test] with NUnit |
| PlayMode | Tests/PlayMode/ | Needs Awake/Start/Update, scene loading, physics, or coroutines | [UnityTest] + IEnumerator |
Default to EditMode whenever possible — they're faster, more reliable, and easier to debug. Only use PlayMode when the code under test depends on Unity's runtime lifecycle.
Each Gherkin scenario from the GDD becomes one test method. The Given/When/Then structure maps directly to Arrange/Act/Assert:
Scenario: Player takes damage
Given a player with 100 health
When the player takes 30 damage
Then the player health should be 70using NUnit.Framework;
namespace {RootNamespace}.{FeatureName}.Tests
{
[TestFixture]
public class HealthSystemTests
{
[Test]
public void TakeDamage_WithFullHealth_ReducesHealthByDamageAmount()
{
// Given
var health = new HealthSystem(maxHealth: 100);
// When
health.TakeDamage(30);
// Then
Assert.AreEqual(70, health.CurrentHealth);
}
}
}MethodName_StateUnderTest_ExpectedBehavior| Part | Purpose | Example |
|---|---|---|
| MethodName | The method being tested | TakeDamage |
| StateUnderTest | The precondition or context | WhenHealthIsZero |
| ExpectedBehavior | What should happen | TriggersDeathEvent |
Examples:
TakeDamage_WhenHealthIsZero_TriggersDeathEventJump_WhenGrounded_AppliesUpwardForceAddItem_WhenInventoryFull_ReturnsFalseSetHealth_WithNegativeValue_ClampsToZeroUse NUnit lifecycle attributes to share setup/cleanup across tests in a fixture:
[TestFixture]
public class WeaponSystemTests
{
private WeaponSystem _weapon;
private HealthSystem _target;
[OneTimeSetUp]
public void OneTimeSetUp()
{
// Runs once before ALL tests in this fixture.
// Use for expensive setup shared across all tests (loading data, etc.)
}
[SetUp]
public void SetUp()
{
// Runs before EACH test — fresh state every time.
_weapon = new WeaponSystem(damage: 25, fireRate: 0.5f);
_target = new HealthSystem(maxHealth: 100);
}
[TearDown]
public void TearDown()
{
// Runs after EACH test — clean up subscriptions, reset state.
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
// Runs once after ALL tests in this fixture.
}
[Test]
public void Attack_WithLoadedWeapon_DealsDamageToTarget()
{
_weapon.Attack(_target);
Assert.AreEqual(75, _target.CurrentHealth);
}
}When to use which:
[SetUp] / [TearDown] — most tests need these. Fresh state per test prevents test pollution.[OneTimeSetUp] / [OneTimeTearDown] — expensive shared resources (loading ScriptableObject data, building test fixtures). Avoid for mutable state.// Equality
Assert.AreEqual(expected, actual);
Assert.AreNotEqual(notExpected, actual);
// Boolean
Assert.IsTrue(player.IsAlive);
Assert.IsFalse(inventory.IsFull);
// Null
Assert.IsNull(result);
Assert.IsNotNull(spawnedObject);
// Float comparison (with tolerance for floating point)
Assert.AreEqual(0.5f, healthPercent, 0.001f);
// Collection
Assert.Contains(item, inventory.Items);
Assert.AreEqual(3, inventory.Items.Count);
Assert.IsEmpty(inventory.Items);
// Exception
Assert.Throws<ArgumentException>(() => weapon.SetDamage(-1));
// String
StringAssert.Contains("damage", logMessage);
// Range (custom — useful for game values)
Assert.GreaterOrEqual(health.CurrentHealth, 0);
Assert.LessOrEqual(health.CurrentHealth, health.MaxHealth);Every feature should be tested against these common edge cases. Map them from GDD scenarios when available, or add defensively:
// Zero and negative values
[Test]
public void TakeDamage_WithZeroDamage_HealthUnchanged()
{
var health = new HealthSystem(maxHealth: 100);
health.TakeDamage(0);
Assert.AreEqual(100, health.CurrentHealth);
}
[Test]
public void TakeDamage_WithNegativeDamage_ClampsToZero()
{
var health = new HealthSystem(maxHealth: 100);
health.TakeDamage(-10);
Assert.AreEqual(100, health.CurrentHealth);
}
// Boundary values
[Test]
public void TakeDamage_ExactlyMaxHealth_HealthReachesZero()
{
var health = new HealthSystem(maxHealth: 100);
health.TakeDamage(100);
Assert.AreEqual(0, health.CurrentHealth);
}
// Overflow / exceed max
[Test]
public void Heal_BeyondMaxHealth_ClampsToMax()
{
var health = new HealthSystem(maxHealth: 100);
health.TakeDamage(50);
health.Heal(999);
Assert.AreEqual(100, health.CurrentHealth);
}
// Rapid repeated calls
[Test]
public void TakeDamage_CalledMultipleTimes_AccumulatesCorrectly()
{
var health = new HealthSystem(maxHealth: 100);
health.TakeDamage(10);
health.TakeDamage(20);
health.TakeDamage(30);
Assert.AreEqual(40, health.CurrentHealth);
}
// State after death / already dead
[Test]
public void TakeDamage_WhenAlreadyDead_NoFurtherDamage()
{
var health = new HealthSystem(maxHealth: 100);
health.TakeDamage(100);
health.TakeDamage(50);
Assert.AreEqual(0, health.CurrentHealth);
}
// Null / missing references (when applicable)
[Test]
public void Attack_WithNullTarget_DoesNotThrow()
{
var weapon = new WeaponSystem(damage: 25);
Assert.DoesNotThrow(() => weapon.Attack(null));
}PlayMode tests run inside Unity's runtime and have access to MonoBehaviour lifecycle, physics, and scene loading. Use [UnityTest] which returns IEnumerator:
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace {RootNamespace}.{FeatureName}.Tests
{
[TestFixture]
public class PlayerMovementPlayModeTests
{
private GameObject _playerObject;
private PlayerMovement _movement;
[SetUp]
public void SetUp()
{
_playerObject = new GameObject("TestPlayer");
_playerObject.AddComponent<Rigidbody>();
_movement = _playerObject.AddComponent<PlayerMovement>();
}
[TearDown]
public void TearDown()
{
Object.DestroyImmediate(_playerObject);
}
[UnityTest]
public IEnumerator Jump_WhenGrounded_MovesPlayerUpward()
{
// Given — player at ground level
float startY = _playerObject.transform.position.y;
// When — jump and wait for physics
_movement.Jump();
yield return new WaitForFixedUpdate();
yield return new WaitForFixedUpdate();
// Then — player should be higher
Assert.Greater(_playerObject.transform.position.y, startY);
}
[UnityTest]
public IEnumerator TakeDamage_WhenPlayerDies_DisablesMovement()
{
// Given — player alive
Assert.IsTrue(_movement.enabled);
// When
_movement.GetComponent<HealthSystem>().TakeDamage(999);
yield return null; // Wait one frame for death logic
// Then
Assert.IsFalse(_movement.enabled);
}
}
}PlayMode tips:
yield return null — wait one frame (for Update logic to run).yield return new WaitForFixedUpdate() — wait for physics step.yield return new WaitForSeconds(0.5f) — wait real time (avoid in CI — use frame counts instead).DestroyImmediate test objects in [TearDown] to prevent leaks.When architecture_pattern: "di-first", test by constructing objects directly with mock dependencies — no container needed in tests:
[Test]
public void ScoreController_OnHit_AddsPoints()
{
// Create mocks / stubs
var mockAudio = new MockAudioService();
var scoreController = new ScoreController(mockAudio);
scoreController.OnHit(pointValue: 10);
Assert.AreEqual(10, scoreController.TotalScore);
}Interface-based DI makes testing simple because every dependency is swappable. See uw-dependency-injection for the interface-first pattern.
run_tests if available, otherwise instruct user to use Test Runner window.uw-code-review to verify test coverage meets GDD Gherkin scenarios.uw-unity-debugging for systematic diagnosis of test failures.uw-unity-feature-scaffold to create the test assembly.MethodName_StateUnderTest_ExpectedBehavior convention.[Test] method.[SetUp] for fresh state per test — avoid shared mutable state between tests.DestroyImmediate all created GameObjects in PlayMode [TearDown]..Tests.asmdef (per NAMING_CONVENTIONS.md).Assert.AreEqual(expected, actual, tolerance) for float comparisons — never exact equality.ProjectConfig.yaml -> mcp.unity_mcp is true, use run_tests to execute. Otherwise, direct user to Test Runner window.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.