mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-13 18:27:05 +02:00
384 lines
8.6 KiB
Markdown
384 lines
8.6 KiB
Markdown
# Unity Test Framework Guide
|
|
|
|
## Overview
|
|
|
|
Unity provides a built-in Test Framework based on NUnit for writing and running automated tests. It supports Edit Mode tests (run without playing) and Play Mode tests (run during gameplay simulation).
|
|
|
|
## Test Framework Setup
|
|
|
|
### Package Installation
|
|
|
|
```json
|
|
// manifest.json - usually pre-installed
|
|
{
|
|
"dependencies": {
|
|
"com.unity.test-framework": "1.6.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Project Structure
|
|
|
|
```
|
|
Assets/
|
|
├── Scripts/
|
|
│ └── Runtime/
|
|
│ ├── Player/
|
|
│ │ └── PlayerController.cs
|
|
│ └── Combat/
|
|
│ └── DamageCalculator.cs
|
|
└── Tests/
|
|
├── EditMode/
|
|
│ └── DamageCalculatorTests.cs
|
|
└── PlayMode/
|
|
└── PlayerMovementTests.cs
|
|
```
|
|
|
|
### Assembly Definitions
|
|
|
|
Create `.asmdef` files for test assemblies:
|
|
|
|
```json
|
|
// Tests/EditMode/EditModeTests.asmdef
|
|
{
|
|
"name": "EditModeTests",
|
|
"references": ["GameAssembly"],
|
|
"includePlatforms": ["Editor"],
|
|
"defineConstraints": ["UNITY_INCLUDE_TESTS"]
|
|
}
|
|
|
|
// Tests/PlayMode/PlayModeTests.asmdef
|
|
{
|
|
"name": "PlayModeTests",
|
|
"references": ["GameAssembly"],
|
|
"includePlatforms": [],
|
|
"defineConstraints": ["UNITY_INCLUDE_TESTS"]
|
|
}
|
|
```
|
|
|
|
## Edit Mode Tests
|
|
|
|
Edit Mode tests run in the Editor without entering Play Mode. Best for testing pure logic.
|
|
|
|
### Basic Test Structure
|
|
|
|
```csharp
|
|
using NUnit.Framework;
|
|
|
|
[TestFixture]
|
|
public class DamageCalculatorTests
|
|
{
|
|
private DamageCalculator _calculator;
|
|
|
|
[SetUp]
|
|
public void Setup()
|
|
{
|
|
_calculator = new DamageCalculator();
|
|
}
|
|
|
|
[Test]
|
|
public void Calculate_BaseDamage_ReturnsCorrectValue()
|
|
{
|
|
float result = _calculator.Calculate(100f, 1f);
|
|
Assert.AreEqual(100f, result);
|
|
}
|
|
|
|
[Test]
|
|
public void Calculate_CriticalHit_DoublesDamage()
|
|
{
|
|
float result = _calculator.Calculate(100f, multiplier: 2f);
|
|
Assert.AreEqual(200f, result);
|
|
}
|
|
|
|
[TestCase(100f, 0.5f, 50f)]
|
|
[TestCase(100f, 1.5f, 150f)]
|
|
[TestCase(50f, 2f, 100f)]
|
|
public void Calculate_Parameterized_ReturnsExpected(
|
|
float base_, float mult, float expected)
|
|
{
|
|
Assert.AreEqual(expected, _calculator.Calculate(base_, mult));
|
|
}
|
|
}
|
|
```
|
|
|
|
### Testing ScriptableObjects
|
|
|
|
```csharp
|
|
[Test]
|
|
public void WeaponStats_DPS_CalculatesCorrectly()
|
|
{
|
|
var weapon = ScriptableObject.CreateInstance<WeaponStats>();
|
|
weapon.baseDamage = 10f;
|
|
weapon.attacksPerSecond = 2f;
|
|
|
|
Assert.AreEqual(20f, weapon.DPS);
|
|
|
|
// Cleanup
|
|
Object.DestroyImmediate(weapon);
|
|
}
|
|
```
|
|
|
|
## Play Mode Tests
|
|
|
|
Play Mode tests run during gameplay simulation. Required for testing MonoBehaviours, physics, and runtime behavior.
|
|
|
|
### Basic Play Mode Test
|
|
|
|
```csharp
|
|
using System.Collections;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
using UnityEngine.TestTools;
|
|
|
|
public class PlayerMovementTests
|
|
{
|
|
private GameObject _player;
|
|
private PlayerController _controller;
|
|
|
|
[SetUp]
|
|
public void Setup()
|
|
{
|
|
_player = new GameObject("Player");
|
|
_controller = _player.AddComponent<PlayerController>();
|
|
_player.AddComponent<Rigidbody>();
|
|
_player.AddComponent<CapsuleCollider>();
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
Object.Destroy(_player);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Move_WhenInputApplied_ChangesPosition()
|
|
{
|
|
Vector3 startPos = _player.transform.position;
|
|
|
|
_controller.SetInput(Vector2.right);
|
|
|
|
yield return new WaitForSeconds(0.5f);
|
|
|
|
Assert.Greater(_player.transform.position.x, startPos.x);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Jump_WhenGrounded_BecomesAirborne()
|
|
{
|
|
// Setup ground
|
|
var ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
|
|
_player.transform.position = Vector3.up;
|
|
|
|
yield return new WaitForFixedUpdate();
|
|
|
|
_controller.Jump();
|
|
|
|
yield return new WaitForSeconds(0.1f);
|
|
|
|
Assert.IsFalse(_controller.IsGrounded);
|
|
|
|
Object.Destroy(ground);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Testing Coroutines
|
|
|
|
```csharp
|
|
[UnityTest]
|
|
public IEnumerator Attack_Cooldown_PreventsSpam()
|
|
{
|
|
_controller.Attack();
|
|
Assert.IsTrue(_controller.IsAttacking);
|
|
|
|
_controller.Attack(); // Should be blocked
|
|
Assert.AreEqual(1, _controller.AttackCount);
|
|
|
|
yield return new WaitForSeconds(_controller.AttackCooldown + 0.1f);
|
|
|
|
_controller.Attack();
|
|
Assert.AreEqual(2, _controller.AttackCount);
|
|
}
|
|
```
|
|
|
|
### Scene Testing
|
|
|
|
```csharp
|
|
using UnityEngine.SceneManagement;
|
|
|
|
[UnityTest]
|
|
public IEnumerator MainMenu_StartButton_LoadsGameScene()
|
|
{
|
|
SceneManager.LoadScene("MainMenu");
|
|
yield return null; // Wait for scene load
|
|
|
|
var startButton = GameObject.Find("StartButton")
|
|
.GetComponent<Button>();
|
|
startButton.onClick.Invoke();
|
|
|
|
yield return new WaitForSeconds(1f);
|
|
|
|
Assert.AreEqual("GameScene", SceneManager.GetActiveScene().name);
|
|
}
|
|
```
|
|
|
|
## Integration Test Patterns
|
|
|
|
### Prefab Testing
|
|
|
|
```csharp
|
|
[UnityTest]
|
|
public IEnumerator EnemyPrefab_Spawns_WithCorrectComponents()
|
|
{
|
|
var prefab = Resources.Load<GameObject>("Prefabs/Enemy");
|
|
var instance = Object.Instantiate(prefab);
|
|
|
|
yield return null;
|
|
|
|
Assert.IsNotNull(instance.GetComponent<EnemyAI>());
|
|
Assert.IsNotNull(instance.GetComponent<Health>());
|
|
Assert.IsNotNull(instance.GetComponent<NavMeshAgent>());
|
|
|
|
Object.Destroy(instance);
|
|
}
|
|
```
|
|
|
|
### Input System Testing
|
|
|
|
```csharp
|
|
using UnityEngine.InputSystem;
|
|
|
|
[UnityTest]
|
|
public IEnumerator InputAction_Fire_TriggersWeapon()
|
|
{
|
|
var keyboard = InputSystem.AddDevice<Keyboard>();
|
|
|
|
yield return null;
|
|
|
|
Press(keyboard.spaceKey);
|
|
yield return null;
|
|
|
|
Assert.IsTrue(_controller.IsFiring);
|
|
|
|
Release(keyboard.spaceKey);
|
|
yield return null;
|
|
|
|
Assert.IsFalse(_controller.IsFiring);
|
|
}
|
|
```
|
|
|
|
## Test Utilities
|
|
|
|
### Custom Assertions
|
|
|
|
```csharp
|
|
public static class GameAssert
|
|
{
|
|
public static void AreApproximatelyEqual(
|
|
Vector3 expected, Vector3 actual, float tolerance = 0.001f)
|
|
{
|
|
Assert.AreEqual(expected.x, actual.x, tolerance);
|
|
Assert.AreEqual(expected.y, actual.y, tolerance);
|
|
Assert.AreEqual(expected.z, actual.z, tolerance);
|
|
}
|
|
|
|
public static void IsWithinRange(float value, float min, float max)
|
|
{
|
|
Assert.GreaterOrEqual(value, min);
|
|
Assert.LessOrEqual(value, max);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Test Fixtures
|
|
|
|
```csharp
|
|
public class TestScene : IDisposable
|
|
{
|
|
public GameObject Player { get; private set; }
|
|
public GameObject Ground { get; private set; }
|
|
|
|
public TestScene()
|
|
{
|
|
Ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
|
|
Player = Object.Instantiate(Resources.Load<GameObject>("Player"));
|
|
Player.transform.position = Vector3.up;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Object.Destroy(Player);
|
|
Object.Destroy(Ground);
|
|
}
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator Player_FallsToGround()
|
|
{
|
|
using var scene = new TestScene();
|
|
|
|
yield return new WaitForSeconds(1f);
|
|
|
|
Assert.Less(scene.Player.transform.position.y, 0.5f);
|
|
}
|
|
```
|
|
|
|
## CI Integration
|
|
|
|
### Command Line Execution
|
|
|
|
```bash
|
|
# Run Edit Mode tests
|
|
Unity -runTests -batchmode -projectPath . \
|
|
-testPlatform EditMode \
|
|
-testResults results.xml
|
|
|
|
# Run Play Mode tests
|
|
Unity -runTests -batchmode -projectPath . \
|
|
-testPlatform PlayMode \
|
|
-testResults results.xml
|
|
```
|
|
|
|
### GitHub Actions
|
|
|
|
```yaml
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: game-ci/unity-test-runner@v4
|
|
with:
|
|
projectPath: .
|
|
testMode: all
|
|
artifactsPath: TestResults
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### DO
|
|
|
|
- Test pure logic in Edit Mode (faster execution)
|
|
- Use Play Mode only when needed (physics, coroutines, MonoBehaviour)
|
|
- Create test fixtures for common setups
|
|
- Clean up created GameObjects in TearDown
|
|
- Use `[Category]` attributes for test organization
|
|
- Run tests before every commit
|
|
|
|
### DON'T
|
|
|
|
- Don't test Unity's built-in functionality
|
|
- Don't rely on specific frame timing (use WaitForSeconds)
|
|
- Don't leave test objects in scenes
|
|
- Don't test private methods directly (test through public API)
|
|
- Don't create tests that depend on execution order
|
|
|
|
## Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
| Issue | Cause | Fix |
|
|
| ---------------------- | ------------------ | ------------------------------------------ |
|
|
| Tests not appearing | Missing asmdef | Create test assembly definition |
|
|
| NullReferenceException | Missing Setup | Ensure [SetUp] initializes all fields |
|
|
| Tests hang | Infinite coroutine | Add timeout or max iterations |
|
|
| Flaky physics tests | Timing dependent | Use WaitForFixedUpdate, increase tolerance |
|