摄像机区域的架构改动

This commit is contained in:
2026-05-15 14:47:24 +08:00
parent 1b37297585
commit f264329751
3591 changed files with 1687228 additions and 446503 deletions

View File

@@ -0,0 +1,384 @@
# 单元测试 01 · SO 事件系统 & ServiceLocator
> **测试类型**EditMode 单元测试NUnit
> **测试文件**`Assets/Tests/EditMode/EventSystemTests.cs`
> **被测程序集**`BaseGames.Core.Events`
> **asmdef 依赖配置**`BaseGames.Tests.EditMode.asmdef` 需引用 `BaseGames.Core.Events`
---
## 目录
1. [测试覆盖范围](#1-测试覆盖范围)
2. [EventSubscription & CompositeDisposable 测试](#2-eventsubscription--compositedisposable-测试)
3. [BaseEventChannelSO 测试](#3-baseeventchannelso-测试)
4. [ServiceLocator 测试](#4-servicelocator-测试)
5. [完整测试代码](#5-完整测试代码)
---
## 1. 测试覆盖范围
| 类 | 测试点 |
|----|--------|
| `EventSubscription` | Dispose 后自动取消回调 |
| `CompositeDisposable` | Clear/Dispose 批量取消Add 后 Clear 清空 |
| `EventSubscriptionExtensions.AddTo` | 返回原订阅句柄(链式调用不丢失引用) |
| `BaseEventChannelSO<T>` | Raise 触发所有订阅者;订阅者为 0 时 Raise 不抛异常 |
| `VoidBaseEventChannelSO` | 同上(无负载版) |
| `ServiceLocator` | Register/Get/Unregister未注册时 Get 抛异常RegisterIfAbsent 防重复 |
---
## 2. EventSubscription & CompositeDisposable 测试
### 测试说明
`EventSubscription` 是纯 C# struct完全无 Unity 依赖,适合 EditMode 测试。
**关键验证点:**
- `Dispose()` 调用 unsubscribe Action
- `CompositeDisposable.Clear()` 遍历调用所有子项的 `Dispose()`
- `AddTo()` 扩展方法正确将订阅加入 CompositeDisposable
---
## 3. BaseEventChannelSO 测试
### 测试说明
`BaseEventChannelSO<T>` 继承 `ScriptableObject`,在 EditMode 中需要用 `ScriptableObject.CreateInstance<T>()` 创建实例(不能 `new`)。
**创建方式:**
```csharp
var channel = ScriptableObject.CreateInstance<IntEventChannelSO>();
```
> ⚠️ 测试结束后必须调用 `Object.DestroyImmediate(channel)` 防止内存泄漏。
---
## 4. ServiceLocator 测试
### 测试说明
`ServiceLocator` 是静态类,测试间有状态污染风险。每个测试前必须调用 `ServiceLocator.Reset()` 清空状态(已在 `#if UNITY_EDITOR` 块内提供)。
**关键验证点:**
- `Register<T>``Get<T>()` 返回正确实例
- `Get<T>()` 未注册时抛出 `InvalidOperationException`
- `GetOrDefault<T>()` 未注册时返回 `default` 不抛异常
- `RegisterIfAbsent<T>()` 重复注册时不覆盖
- `Unregister<T>(impl)` 只在实例匹配时才移除
---
## 5. 完整测试代码
将以下代码保存至 `Assets/Tests/EditMode/EventSystemTests.cs`
```csharp
using System;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using BaseGames.Core.Events;
using BaseGames.Core;
namespace BaseGames.Tests.EditMode
{
/// <summary>
/// SO 事件系统 + ServiceLocator 单元测试EditMode无需 Play Mode
/// </summary>
[TestFixture]
public class EventSystemTests
{
// ── EventSubscription ────────────────────────────────────────────────
[Test]
public void EventSubscription_Dispose_CallsUnsubscribeAction()
{
bool called = false;
var sub = new EventSubscription(() => called = true);
sub.Dispose();
Assert.IsTrue(called, "Dispose 应调用 unsubscribe action");
}
[Test]
public void EventSubscription_Dispose_NullAction_DoesNotThrow()
{
var sub = new EventSubscription(null);
Assert.DoesNotThrow(() => sub.Dispose());
}
// ── CompositeDisposable ──────────────────────────────────────────────
[Test]
public void CompositeDisposable_Clear_DisposesAllItems()
{
var composite = new CompositeDisposable();
int count = 0;
composite.Add(new EventSubscription(() => count++));
composite.Add(new EventSubscription(() => count++));
composite.Add(new EventSubscription(() => count++));
composite.Clear();
Assert.AreEqual(3, count, "Clear 应 Dispose 所有已添加的订阅");
}
[Test]
public void CompositeDisposable_Clear_TwiceSafe()
{
var composite = new CompositeDisposable();
int count = 0;
composite.Add(new EventSubscription(() => count++));
composite.Clear();
composite.Clear(); // 第二次 Clear 不应重复 Dispose
Assert.AreEqual(1, count, "Clear 两次不应重复 Dispose");
}
[Test]
public void CompositeDisposable_Dispose_SameAsClear()
{
var composite = new CompositeDisposable();
bool disposed = false;
composite.Add(new EventSubscription(() => disposed = true));
((IDisposable)composite).Dispose();
Assert.IsTrue(disposed);
}
[Test]
public void AddTo_ReturnsOriginalSubscription()
{
var composite = new CompositeDisposable();
bool called = false;
var sub = new EventSubscription(() => called = true);
var returned = sub.AddTo(composite);
returned.Dispose();
Assert.IsTrue(called, "AddTo 返回的订阅 Dispose 后应触发原 unsubscribe");
}
[Test]
public void AddTo_Collection_AddsToList()
{
var list = new List<IDisposable>();
bool called = false;
var sub = new EventSubscription(() => called = true);
sub.AddTo(list);
Assert.AreEqual(1, list.Count, "AddTo(ICollection) 应将订阅添加到列表");
list[0].Dispose();
Assert.IsTrue(called);
}
// ── IntEventChannelSO ────────────────────────────────────────────────
[Test]
public void IntEventChannel_Raise_CallsAllSubscribers()
{
var channel = ScriptableObject.CreateInstance<IntEventChannelSO>();
try
{
int sum = 0;
channel.OnEventRaised += v => sum += v;
channel.OnEventRaised += v => sum += v;
channel.Raise(5);
Assert.AreEqual(10, sum, "两个订阅者应各收到一次事件");
}
finally
{
Object.DestroyImmediate(channel);
}
}
[Test]
public void IntEventChannel_Raise_NoSubscribers_DoesNotThrow()
{
var channel = ScriptableObject.CreateInstance<IntEventChannelSO>();
try
{
Assert.DoesNotThrow(() => channel.Raise(42));
}
finally
{
Object.DestroyImmediate(channel);
}
}
[Test]
public void IntEventChannel_UnsubscribeAfterDispose()
{
var channel = ScriptableObject.CreateInstance<IntEventChannelSO>();
try
{
int callCount = 0;
void Handler(int v) => callCount++;
channel.OnEventRaised += Handler;
channel.Raise(1);
Assert.AreEqual(1, callCount);
channel.OnEventRaised -= Handler;
channel.Raise(1);
Assert.AreEqual(1, callCount, "取消订阅后不应再收到事件");
}
finally
{
Object.DestroyImmediate(channel);
}
}
// ── VoidEventChannelSO ───────────────────────────────────────────────
[Test]
public void VoidEventChannel_Raise_CallsSubscriber()
{
var channel = ScriptableObject.CreateInstance<VoidEventChannelSO>();
try
{
bool received = false;
channel.OnEventRaised += () => received = true;
channel.Raise();
Assert.IsTrue(received);
}
finally
{
Object.DestroyImmediate(channel);
}
}
// ── ServiceLocator ───────────────────────────────────────────────────
[SetUp]
public void SetUp() => ServiceLocator.Reset();
[TearDown]
public void TearDown() => ServiceLocator.Reset();
[Test]
public void ServiceLocator_RegisterAndGet_ReturnsInstance()
{
var mock = new MockService();
ServiceLocator.Register<IMockService>(mock);
var retrieved = ServiceLocator.Get<IMockService>();
Assert.AreSame(mock, retrieved);
}
[Test]
public void ServiceLocator_GetUnregistered_ThrowsInvalidOperation()
{
Assert.Throws<InvalidOperationException>(
() => ServiceLocator.Get<IMockService>());
}
[Test]
public void ServiceLocator_GetOrDefault_Unregistered_ReturnsDefault()
{
var result = ServiceLocator.GetOrDefault<IMockService>();
Assert.IsNull(result);
}
[Test]
public void ServiceLocator_GetOrDefault_Registered_ReturnsInstance()
{
var mock = new MockService();
ServiceLocator.Register<IMockService>(mock);
var result = ServiceLocator.GetOrDefault<IMockService>();
Assert.AreSame(mock, result);
}
[Test]
public void ServiceLocator_RegisterIfAbsent_DoesNotOverwrite()
{
var first = new MockService();
var second = new MockService();
ServiceLocator.Register<IMockService>(first);
ServiceLocator.RegisterIfAbsent<IMockService>(second);
Assert.AreSame(first, ServiceLocator.Get<IMockService>(),
"RegisterIfAbsent 不应覆盖已存在的注册");
}
[Test]
public void ServiceLocator_Unregister_RemovesService()
{
ServiceLocator.Register<IMockService>(new MockService());
ServiceLocator.Unregister<IMockService>();
Assert.Throws<InvalidOperationException>(
() => ServiceLocator.Get<IMockService>());
}
[Test]
public void ServiceLocator_Unregister_WithWrongInstance_DoesNotRemove()
{
var first = new MockService();
var second = new MockService();
ServiceLocator.Register<IMockService>(first);
ServiceLocator.Unregister<IMockService>(second); // 不同实例,不应移除
Assert.AreSame(first, ServiceLocator.Get<IMockService>(),
"错误实例的 Unregister 不应移除已注册的服务");
}
[Test]
public void ServiceLocator_OverrideForTest_ReplacesService()
{
var original = new MockService { Value = 1 };
var replacement = new MockService { Value = 99 };
ServiceLocator.Register<IMockService>(original);
ServiceLocator.OverrideForTest<IMockService>(replacement);
Assert.AreEqual(99, ServiceLocator.Get<IMockService>().Value);
}
// ── Test Helpers ─────────────────────────────────────────────────────
private interface IMockService { int Value { get; set; } }
private class MockService : IMockService { public int Value { get; set; } }
}
}
```
---
## 运行方式
1. 将上方代码保存至 `Assets/Tests/EditMode/EventSystemTests.cs`
2. 打开 `Window → General → Test Runner`
3. 选择 `EditMode` → 展开 `BaseGames.Tests.EditMode` → 展开 `EventSystemTests`
4. 点击 `Run All` 或逐个运行
### asmdef 配置确认
打开 `Assets/Tests/EditMode/BaseGames.Tests.EditMode.asmdef`,确认 `references` 数组包含:
```json
{
"references": [
"BaseGames.Core.Events",
"BaseGames.Core",
"BaseGames.Combat.StatusEffects"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": ["nunit.framework.dll"],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
```
---
## 预期结果
所有 **20 个测试** 全部通过(绿色)。
若出现失败,常见原因:
| 失败现象 | 排查方向 |
|---------|---------|
| `ServiceLocator.Reset()` 方法不存在 | 检查 `#if UNITY_EDITOR` 编译条件是否生效EditMode 测试应在 UNITY_EDITOR 下) |
| `IntEventChannelSO` 找不到 | 确认 asmdef references 已包含 `BaseGames.Core.Events` |
| `NullReferenceException` on `channel.Raise` | 确认使用 `ScriptableObject.CreateInstance<>()` 而非 `new` |