# 单元测试 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` | 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` 继承 `ScriptableObject`,在 EditMode 中需要用 `ScriptableObject.CreateInstance()` 创建实例(不能 `new`)。 **创建方式:** ```csharp var channel = ScriptableObject.CreateInstance(); ``` > ⚠️ 测试结束后必须调用 `Object.DestroyImmediate(channel)` 防止内存泄漏。 --- ## 4. ServiceLocator 测试 ### 测试说明 `ServiceLocator` 是静态类,测试间有状态污染风险。每个测试前必须调用 `ServiceLocator.Reset()` 清空状态(已在 `#if UNITY_EDITOR` 块内提供)。 **关键验证点:** - `Register` 后 `Get()` 返回正确实例 - `Get()` 未注册时抛出 `InvalidOperationException` - `GetOrDefault()` 未注册时返回 `default` 不抛异常 - `RegisterIfAbsent()` 重复注册时不覆盖 - `Unregister(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 { /// /// SO 事件系统 + ServiceLocator 单元测试(EditMode,无需 Play Mode)。 /// [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(); 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(); 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(); try { Assert.DoesNotThrow(() => channel.Raise(42)); } finally { Object.DestroyImmediate(channel); } } [Test] public void IntEventChannel_UnsubscribeAfterDispose() { var channel = ScriptableObject.CreateInstance(); 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(); 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(mock); var retrieved = ServiceLocator.Get(); Assert.AreSame(mock, retrieved); } [Test] public void ServiceLocator_GetUnregistered_ThrowsInvalidOperation() { Assert.Throws( () => ServiceLocator.Get()); } [Test] public void ServiceLocator_GetOrDefault_Unregistered_ReturnsDefault() { var result = ServiceLocator.GetOrDefault(); Assert.IsNull(result); } [Test] public void ServiceLocator_GetOrDefault_Registered_ReturnsInstance() { var mock = new MockService(); ServiceLocator.Register(mock); var result = ServiceLocator.GetOrDefault(); Assert.AreSame(mock, result); } [Test] public void ServiceLocator_RegisterIfAbsent_DoesNotOverwrite() { var first = new MockService(); var second = new MockService(); ServiceLocator.Register(first); ServiceLocator.RegisterIfAbsent(second); Assert.AreSame(first, ServiceLocator.Get(), "RegisterIfAbsent 不应覆盖已存在的注册"); } [Test] public void ServiceLocator_Unregister_RemovesService() { ServiceLocator.Register(new MockService()); ServiceLocator.Unregister(); Assert.Throws( () => ServiceLocator.Get()); } [Test] public void ServiceLocator_Unregister_WithWrongInstance_DoesNotRemove() { var first = new MockService(); var second = new MockService(); ServiceLocator.Register(first); ServiceLocator.Unregister(second); // 不同实例,不应移除 Assert.AreSame(first, ServiceLocator.Get(), "错误实例的 Unregister 不应移除已注册的服务"); } [Test] public void ServiceLocator_OverrideForTest_ReplacesService() { var original = new MockService { Value = 1 }; var replacement = new MockService { Value = 99 }; ServiceLocator.Register(original); ServiceLocator.OverrideForTest(replacement); Assert.AreEqual(99, ServiceLocator.Get().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` |