tools-unity-test-framework
Unity Test Framework patterns for EditMode and PlayMode tests including async testing, mocking, and test organization.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Unity Test Framework patterns for EditMode and PlayMode tests including async testing, mocking, and test organization.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Creates an Architecture Decision Record (ADR) documenting a significant technical decision, its context, alternatives considered, and consequences. Every major technical choice should have an ADR.
Mobile-specific Unity optimization patterns for memory, battery, thermal, and performance.
End-of-story completion review. Reads the story file, verifies each acceptance criterion against the implementation, checks for GDD/ADR deviations, prompts code review, updates story status to Complete, and surfaces the next ready story from the sprint.
Unity Addressables patterns for asset loading, memory management, reference counting, and remote content delivery.
Unity animation patterns including Animancer, state machines, and performance optimization.
Behavior Designer patterns for AI behavior trees including task creation, shared variables, conditionals, and debugging.
| name | tools-unity-test-framework |
| description | Unity Test Framework patterns for EditMode and PlayMode tests including async testing, mocking, and test organization. |
Unity Test Framework provides NUnit-based testing for Unity with EditMode (fast, no runtime) and PlayMode (full Unity lifecycle) test support.
// Tests/Editor/MyGame.Tests.Editor.asmdef
{
"name": "MyGame.Tests.Editor",
"rootNamespace": "MyGame.Tests",
"references": [
"MyGame.Core",
"MyGame.Gameplay",
"VContainer",
"UniTask"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll",
"NSubstitute.dll"
],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}
// Tests/Runtime/MyGame.Tests.Runtime.asmdef
{
"name": "MyGame.Tests.Runtime",
"references": [
"MyGame.Core",
"MyGame.Gameplay",
"VContainer",
"UniTask"
],
"includePlatforms": [],
"excludePlatforms": [],
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll",
"NSubstitute.dll"
],
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
]
}
using NUnit.Framework;
namespace MyGame.Tests
{
[TestFixture]
public class DamageCalculatorTests
{
private DamageCalculator _calculator;
[SetUp]
public void SetUp()
{
_calculator = new DamageCalculator();
}
[TearDown]
public void TearDown()
{
_calculator = null;
}
[Test]
public void CalculateDamage_WithCrit_DoublesBaseDamage()
{
// Arrange
var baseDamage = 100;
var isCrit = true;
// Act
var result = _calculator.Calculate(baseDamage, isCrit);
// Assert
Assert.AreEqual(200, result);
}
[Test]
public void CalculateDamage_NoCrit_ReturnsBaseDamage()
{
var result = _calculator.Calculate(100, false);
Assert.AreEqual(100, result);
}
}
}
[TestFixture]
public class AttributeTests
{
[TestCase(100, 0.5f, 50)]
[TestCase(100, 1.0f, 100)]
[TestCase(100, 2.0f, 200)]
[TestCase(0, 1.5f, 0)]
public void ApplyMultiplier_ReturnsCorrectValue(float baseValue, float multiplier, float expected)
{
var result = AttributeCalculator.ApplyMultiplier(baseValue, multiplier);
Assert.AreEqual(expected, result, 0.001f);
}
[TestCaseSource(nameof(DamageTestCases))]
public void CalculateDamage_WithTestCases(int attack, int defense, int expected)
{
var result = DamageFormula.Calculate(attack, defense);
Assert.AreEqual(expected, result);
}
private static IEnumerable<TestCaseData> DamageTestCases()
{
yield return new TestCaseData(100, 50, 75).SetName("Normal damage");
yield return new TestCaseData(100, 100, 50).SetName("Equal stats");
yield return new TestCaseData(50, 100, 25).SetName("High defense");
}
}
[TestFixture]
public class HealthComponentTests
{
private GameObject _testObject;
private HealthComponent _health;
[SetUp]
public void SetUp()
{
_testObject = new GameObject("TestHealth");
_health = _testObject.AddComponent<HealthComponent>();
_health.Initialize(100);
}
[TearDown]
public void TearDown()
{
Object.DestroyImmediate(_testObject);
}
[Test]
public void TakeDamage_ReducesHealth()
{
_health.TakeDamage(30);
Assert.AreEqual(70, _health.CurrentHealth);
}
[Test]
public void TakeDamage_AtZero_TriggersDeath()
{
bool deathTriggered = false;
_health.OnDeath += () => deathTriggered = true;
_health.TakeDamage(100);
Assert.IsTrue(deathTriggered);
Assert.AreEqual(0, _health.CurrentHealth);
}
}
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
[TestFixture]
public class PlayerMovementTests
{
private GameObject _player;
private PlayerMovement _movement;
[UnitySetUp]
public IEnumerator SetUp()
{
_player = new GameObject("Player");
_movement = _player.AddComponent<PlayerMovement>();
yield return null; // Wait one frame for Awake/Start
}
[UnityTearDown]
public IEnumerator TearDown()
{
Object.Destroy(_player);
yield return null;
}
[UnityTest]
public IEnumerator Move_OverTime_ChangesPosition()
{
var startPos = _player.transform.position;
_movement.Move(Vector3.forward);
yield return new WaitForSeconds(0.5f);
Assert.AreNotEqual(startPos, _player.transform.position);
}
}
using Cysharp.Threading.Tasks;
[TestFixture]
public class AsyncServiceTests
{
[Test]
public async Task LoadDataAsync_ReturnsValidData()
{
var service = new DataService();
var result = await service.LoadDataAsync();
Assert.IsNotNull(result);
Assert.IsTrue(result.IsValid);
}
[UnityTest]
public IEnumerator LoadDataAsync_WithUniTask_Works() => UniTask.ToCoroutine(async () =>
{
var service = new DataService();
var result = await service.LoadDataAsync();
Assert.IsNotNull(result);
});
}
using NSubstitute;
[TestFixture]
public class CombatSystemTests
{
private ICombatSystem _combatSystem;
private ITargetingService _mockTargeting;
private IDamageCalculator _mockDamage;
[SetUp]
public void SetUp()
{
_mockTargeting = Substitute.For<ITargetingService>();
_mockDamage = Substitute.For<IDamageCalculator>();
_combatSystem = new CombatSystem(_mockTargeting, _mockDamage);
}
[Test]
public void Attack_WithValidTarget_CalculatesDamage()
{
// Arrange
var attacker = CreateTestCharacter();
var target = CreateTestCharacter();
_mockTargeting.GetTarget(attacker).Returns(target);
_mockDamage.Calculate(Arg.Any<AttackData>()).Returns(50);
// Act
_combatSystem.Attack(attacker);
// Assert
_mockDamage.Received(1).Calculate(Arg.Any<AttackData>());
}
[Test]
public void Attack_WithNoTarget_DoesNotCalculateDamage()
{
var attacker = CreateTestCharacter();
_mockTargeting.GetTarget(attacker).Returns((Character)null);
_combatSystem.Attack(attacker);
_mockDamage.DidNotReceive().Calculate(Arg.Any<AttackData>());
}
}
[Test]
public async Task SaveGame_CallsStorageService()
{
var mockStorage = Substitute.For<IStorageService>();
mockStorage.SaveAsync(Arg.Any<SaveData>()).Returns(UniTask.CompletedTask);
var saveSystem = new SaveSystem(mockStorage);
await saveSystem.SaveGameAsync();
await mockStorage.Received(1).SaveAsync(Arg.Any<SaveData>());
}
[Test]
public void ApplyDamage_WithCorrectAmount()
{
var mockHealth = Substitute.For<IHealthComponent>();
var combat = new CombatHandler(mockHealth);
combat.ApplyDamage(100);
// Exact value
mockHealth.Received().TakeDamage(100);
// Any value
mockHealth.Received().TakeDamage(Arg.Any<int>());
// Value matching condition
mockHealth.Received().TakeDamage(Arg.Is<int>(x => x > 0));
}
[TestFixture]
public class ServiceTests
{
private LifetimeScope _testScope;
[SetUp]
public void SetUp()
{
_testScope = LifetimeScope.Create(builder =>
{
// Register mocks
var mockRepo = Substitute.For<IPlayerRepository>();
builder.RegisterInstance(mockRepo);
// Register real service under test
builder.Register<PlayerService>(Lifetime.Singleton);
});
}
[TearDown]
public void TearDown()
{
_testScope?.Dispose();
}
[Test]
public void PlayerService_ResolvesCorrectly()
{
var service = _testScope.Container.Resolve<PlayerService>();
Assert.IsNotNull(service);
}
}
[Test]
public void GameInstaller_RegistersAllDependencies()
{
using var scope = LifetimeScope.Create(new GameInstaller());
// Verify critical services resolve
Assert.DoesNotThrow(() => scope.Container.Resolve<IPlayerService>());
Assert.DoesNotThrow(() => scope.Container.Resolve<ICombatService>());
Assert.DoesNotThrow(() => scope.Container.Resolve<IInventoryService>());
}
public abstract class GameplayTestBase
{
protected GameObject TestGameObject { get; private set; }
[SetUp]
public virtual void SetUp()
{
TestGameObject = new GameObject("Test");
}
[TearDown]
public virtual void TearDown()
{
if (TestGameObject != null)
Object.DestroyImmediate(TestGameObject);
}
protected T AddComponent<T>() where T : Component
{
return TestGameObject.AddComponent<T>();
}
protected GameObject CreateChild(string name = "Child")
{
var child = new GameObject(name);
child.transform.SetParent(TestGameObject.transform);
return child;
}
}
// Usage
public class MyTests : GameplayTestBase
{
[Test]
public void Test()
{
var component = AddComponent<MyComponent>();
// Test...
}
}
public static class AsyncTestHelpers
{
public static IEnumerator WaitForCondition(
Func<bool> condition,
float timeout = 5f,
string message = "Condition not met")
{
var startTime = Time.time;
while (!condition())
{
if (Time.time - startTime > timeout)
{
Assert.Fail(message);
yield break;
}
yield return null;
}
}
public static IEnumerator WaitFrames(int frames)
{
for (int i = 0; i < frames; i++)
yield return null;
}
}
public static class ScriptableObjectTestHelper
{
public static T CreateInstance<T>() where T : ScriptableObject
{
return ScriptableObject.CreateInstance<T>();
}
public static T CreateInstance<T>(Action<T> configure) where T : ScriptableObject
{
var instance = ScriptableObject.CreateInstance<T>();
configure(instance);
return instance;
}
}
// Usage
[Test]
public void AbilityConfig_HasCorrectDefaults()
{
var ability = ScriptableObjectTestHelper.CreateInstance<AbilitySO>(a =>
{
a.Damage = 100;
a.Cooldown = 5f;
});
Assert.AreEqual(100, ability.Damage);
}
[TestFixture]
[Category("Combat")]
public class CombatTests
{
[Test]
[Category("Critical")]
public void CriticalCombatTest() { }
[Test]
[Category("Slow")]
public void SlowCombatTest() { }
}
// Run specific category:
// Unity Test Runner > Category filter
// Or CLI: -testFilter "Category=Combat"
[Test]
[Platform(Include = "Editor")]
public void EditorOnlyTest() { }
[Test]
[UnityPlatform(RuntimePlatform.Android, RuntimePlatform.IPhonePlayer)]
public void MobileOnlyTest() { }
[Test]
[Ignore("Not implemented yet")]
public void FutureTest() { }
// Equality
Assert.AreEqual(expected, actual);
Assert.AreNotEqual(unexpected, actual);
// Boolean
Assert.IsTrue(condition);
Assert.IsFalse(condition);
// Null
Assert.IsNull(obj);
Assert.IsNotNull(obj);
// Collections
Assert.Contains(item, collection);
Assert.IsEmpty(collection);
CollectionAssert.AreEqual(expected, actual);
CollectionAssert.AreEquivalent(expected, actual); // Order doesn't matter
// Exceptions
Assert.Throws<ArgumentException>(() => MethodThatThrows());
Assert.DoesNotThrow(() => SafeMethod());
// Floating point (with tolerance)
Assert.AreEqual(1.0f, actual, 0.001f);
// String
StringAssert.Contains("expected", actualString);
StringAssert.StartsWith("prefix", actualString);
public static class GameAssert
{
public static void IsAlive(Character character)
{
Assert.IsTrue(character.IsAlive, $"Expected {character.Name} to be alive");
}
public static void HasTag(GameplayTagContainer container, string tag)
{
Assert.IsTrue(
container.HasTag(GameplayTag.Get(tag)),
$"Expected container to have tag '{tag}'"
);
}
}
# Run EditMode tests
Unity -runTests -batchmode -projectPath /path/to/project \
-testPlatform EditMode \
-testResults results.xml \
-logFile test.log
# Run specific category
Unity -runTests -batchmode -projectPath /path/to/project \
-testPlatform EditMode \
-testFilter "Category=Critical"
# Run PlayMode tests
Unity -runTests -batchmode -projectPath /path/to/project \
-testPlatform PlayMode \
-testResults results.xml
- name: Run Tests
run: |
Unity -runTests \
-batchmode \
-projectPath . \
-testPlatform EditMode \
-testResults TestResults.xml \
-logFile TestLog.txt
- name: Upload Results
uses: actions/upload-artifact@v3
with:
name: test-results
path: TestResults.xml
Method_Scenario_ExpectedResult