13 KiB
13 KiB
单元测试 01 · SO 事件系统 & ServiceLocator
测试类型:EditMode 单元测试(NUnit)
测试文件:Assets/Tests/EditMode/EventSystemTests.cs
被测程序集:BaseGames.Core.Events
asmdef 依赖配置:BaseGames.Tests.EditMode.asmdef需引用BaseGames.Core.Events
目录
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 ActionCompositeDisposable.Clear()遍历调用所有子项的Dispose()AddTo()扩展方法正确将订阅加入 CompositeDisposable
3. BaseEventChannelSO 测试
测试说明
BaseEventChannelSO<T> 继承 ScriptableObject,在 EditMode 中需要用 ScriptableObject.CreateInstance<T>() 创建实例(不能 new)。
创建方式:
var channel = ScriptableObject.CreateInstance<IntEventChannelSO>();
⚠️ 测试结束后必须调用
Object.DestroyImmediate(channel)防止内存泄漏。
4. ServiceLocator 测试
测试说明
ServiceLocator 是静态类,测试间有状态污染风险。每个测试前必须调用 ServiceLocator.Reset() 清空状态(已在 #if UNITY_EDITOR 块内提供)。
关键验证点:
Register<T>后Get<T>()返回正确实例Get<T>()未注册时抛出InvalidOperationExceptionGetOrDefault<T>()未注册时返回default不抛异常RegisterIfAbsent<T>()重复注册时不覆盖Unregister<T>(impl)只在实例匹配时才移除
5. 完整测试代码
将以下代码保存至 Assets/Tests/EditMode/EventSystemTests.cs:
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; } }
}
}
运行方式
- 将上方代码保存至
Assets/Tests/EditMode/EventSystemTests.cs - 打开
Window → General → Test Runner - 选择
EditMode→ 展开BaseGames.Tests.EditMode→ 展开EventSystemTests - 点击
Run All或逐个运行
asmdef 配置确认
打开 Assets/Tests/EditMode/BaseGames.Tests.EditMode.asmdef,确认 references 数组包含:
{
"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 |