摄像机区域的架构改动
This commit is contained in:
384
Docs/Verification/01_Unit_EventSystem_ServiceLocator.md
Normal file
384
Docs/Verification/01_Unit_EventSystem_ServiceLocator.md
Normal 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` |
|
||||
Reference in New Issue
Block a user