重构集成测试为9个有序独立步骤方法 将 IoTZero/ZeroServer 集成测试由单一串行方法重构为 9 个有序 [Fact] 测试方法,提升可维护性和定位效率。WebFactory 增加共享测试状态,步骤间复用客户端与上下文,简化参数传递。统一异步等待写法,优化注释与日志,确保资源正确释放。事件上报等辅助流程也独立为单测,整体风格更一致。大石头 authored at 2026-04-27 15:50:31
diff --git a/XUnitTest.Samples/IoTZero/IoTZeroIntegrationTests.cs b/XUnitTest.Samples/IoTZero/IoTZeroIntegrationTests.cs
index 5a688e2..9956108 100644
--- a/XUnitTest.Samples/IoTZero/IoTZeroIntegrationTests.cs
+++ b/XUnitTest.Samples/IoTZero/IoTZeroIntegrationTests.cs
@@ -15,8 +15,9 @@ namespace XUnitTest.Samples.IoTZero;
/// <summary>
/// IoTZero HTTP 全链路集成测试。
/// 使用 WebApplicationFactory 启动真实 Kestrel(端口自动分配),HttpDevice 通过 HTTP 连接。
-/// 单个测试方法串行执行 9 步,覆盖:自动注册登录、配置持久化、服务端实体验证、注销/重登、
-/// 心跳计数、WebSocket 通知建立、SendCommand 投递、升级检查、事件上报。
+/// 9 个有序 [Fact] 方法(由 DefaultOrderer 按源码顺序执行),共用 IoTZeroWebFactory 持有的客户端状态。
+/// 覆盖:自动注册登录、配置持久化、服务端实体验证、注销/重登、心跳计数、
+/// WebSocket 通知建立、SendCommand 投递、升级检查、事件上报。
/// </summary>
[Collection("SamplesIntegration")]
[TestCaseOrderer("NewLife.UnitTest.DefaultOrderer", "NewLife.UnitTest")]
@@ -26,58 +27,21 @@ public class IoTZeroIntegrationTests : IClassFixture<IoTZeroWebFactory>
public IoTZeroIntegrationTests(IoTZeroWebFactory factory) => _factory = factory;
- #region 9 步串行集成测试
- [Fact(DisplayName = "IoTZero_9步全链路集成测试")]
- public async Task FullIntegrationFlow_AllNineSteps()
+ #region Step 1 — 自动注册登录
+ [Fact(DisplayName = "IoTZero_Step1_自动注册登录")]
+ public async Task Step1_AutoRegisterLogin()
{
- // 确保 Factory 已启动(首次访问时触发 ConfigureWebHost + CreateHost)
+ // 确保 Factory 已启动
Assert.False(_factory.BaseUrl.IsNullOrEmpty(), "BaseUrl 未初始化,Factory 启动失败");
XTrace.WriteLine("IoTZero 测试服务地址:{0}", _factory.BaseUrl);
- // 清空上次测试残留数据:XCode DAL 连接 IoT 持久化在 AppBase/Data 目录,
- // 跨测试运行累积;若不清理,Step3 的 Assert.Empty(DeviceOnline) 会因旧记录而误判失败
+ // Factory 的 CleanTestData() 已在 InitializeAsync 中执行;
+ // 此处再次清理 DeviceOnline/DeviceHistory,防止同一进程内多次测试运行时的残留(步骤级清理)
DeviceOnline.Delete("1=1");
DeviceHistory.Delete("1=1");
- // 创建新的 ClientSetting(空 DeviceCode/DeviceSecret,触发服务端自动注册)
- var setting = new ClientSetting { Server = _factory.BaseUrl };
-
- using var client = new HttpDevice(setting);
- client.Log = XTrace.Log;
-
- // Step 1 - 自动注册登录
- var (code, secret) = await Step1_AutoRegisterLogin(client, setting);
-
- // Step 2 - 服务端实体验证(通过 XCode 直接查询,同进程内共享 DAL 连接)
- var deviceId = await Step2_VerifyServerEntities(code);
-
- // Step 3 - 注销
- await Step3_Logout(client, deviceId);
-
- // Step 4 - 修改密钥后重登
- await Step4_ReLoginWithNewSecret(client, setting, code, deviceId);
-
- // Step 5 - 心跳
- await Step5_Ping(client, deviceId);
-
- // Step 6 - WebSocket 建立确认
- await Step6_WaitWebSocket(deviceId);
-
- // Step 7 - SendCommand 投递(Timeout=0,服务端立即返回,客户端通过 WebSocket 收到命令)
- await Step7_SendCommandAndReceive(client, code);
-
- // Step 8 - 升级检查
- await Step8_Upgrade(client);
-
- // Step 9 - 事件上报
- await Step9_PostEvents(client);
- }
- #endregion
-
- #region Step 1 — 自动注册登录
- private async Task<(String code, String secret)> Step1_AutoRegisterLogin(HttpDevice client, ClientSetting setting)
- {
- XTrace.WriteLine("=== Step 1: 自动注册登录 ===");
+ var client = _factory.TestClient;
+ var setting = _factory.TestSetting;
// 确保以空 DeviceCode/DeviceSecret 开始,触发服务端自动注册
setting.DeviceCode = null!;
@@ -105,15 +69,19 @@ public class IoTZeroIntegrationTests : IClassFixture<IoTZeroWebFactory>
XTrace.WriteLine("配置文件已写入,内容长度={0}", configContent!.Length);
- return (setting.DeviceCode, setting.DeviceSecret);
+ // 保存 DeviceCode 到 Factory,供后续步骤使用
+ _factory.TestCode = setting.DeviceCode;
}
#endregion
#region Step 2 — 服务端实体验证
- private async Task<Int32> Step2_VerifyServerEntities(String code)
+ [Fact(DisplayName = "IoTZero_Step2_验证服务端实体")]
+ public async Task Step2_VerifyServerEntities()
{
XTrace.WriteLine("=== Step 2: 验证服务端实体 ===");
+ var code = _factory.TestCode;
+
// 等待服务端异步写库完成(Device/DeviceOnline 由 Save 同步写入,无需长等待)
await Task.Delay(300).ConfigureAwait(false);
@@ -141,23 +109,27 @@ public class IoTZeroIntegrationTests : IClassFixture<IoTZeroWebFactory>
XTrace.WriteLine("DeviceOnline 数量={0}", onlines.Count);
- // 验证 DeviceHistory 已记录登录(DefaultDeviceService.OnLogin 写 source+"登录",source=Http)
- // WriteHistory 调用 SaveAsync,XCode EntityQueue ~1秒刷新,需轮询等待
+ // 验证 DeviceHistory 已记录登录(WriteHistory 使用 EntityQueue 异步写库,需轮询等待)
var logins = await WaitForDeviceHistory(device.Id, "Http登录").ConfigureAwait(false);
Assert.NotEmpty(logins);
Assert.True(logins[0].Success, "第一次登录的 DeviceHistory 应为成功");
XTrace.WriteLine("DeviceHistory 登录记录={0}", logins.Count);
- return device.Id;
+ // 保存 DeviceId 到 Factory,供后续步骤使用
+ _factory.TestDeviceId = device.Id;
}
#endregion
#region Step 3 — 注销
- private async Task Step3_Logout(HttpDevice client, Int32 deviceId)
+ [Fact(DisplayName = "IoTZero_Step3_注销")]
+ public async Task Step3_Logout()
{
XTrace.WriteLine("=== Step 3: 注销 ===");
+ var client = _factory.TestClient;
+ var deviceId = _factory.TestDeviceId;
+
await client.Logout("集成测试注销", CancellationToken.None).ConfigureAwait(false);
Assert.False(client.Logined, "Logout 后 Logined 应为 false");
@@ -174,8 +146,7 @@ public class IoTZeroIntegrationTests : IClassFixture<IoTZeroWebFactory>
}
Assert.True(onlines.Count == 0, $"DeviceOnline 应已清除,但有 {onlines.Count} 条记录,SessionIds=[{String.Join(",", onlines.Select(o => o.SessionId))}]");
- // DeviceHistory 应有下线记录(DefaultDeviceService.Logout 写 source+"设备下线",source=Http)
- // WriteHistory 调用 SaveAsync,XCode EntityQueue ~1秒刷新,需轮询等待
+ // DeviceHistory 应有下线记录(WriteHistory 使用 EntityQueue 异步写库,需轮询等待)
var logouts = await WaitForDeviceHistory(deviceId, "Http设备下线").ConfigureAwait(false);
Assert.NotEmpty(logouts);
@@ -184,10 +155,16 @@ public class IoTZeroIntegrationTests : IClassFixture<IoTZeroWebFactory>
#endregion
#region Step 4 — 修改密钥后重登
- private async Task Step4_ReLoginWithNewSecret(HttpDevice client, ClientSetting setting, String code, Int32 deviceId)
+ [Fact(DisplayName = "IoTZero_Step4_修改密钥后重登")]
+ public async Task Step4_ReLoginWithNewSecret()
{
XTrace.WriteLine("=== Step 4: 修改密钥后重登 ===");
+ var client = _factory.TestClient;
+ var setting = _factory.TestSetting;
+ var code = _factory.TestCode;
+ var deviceId = _factory.TestDeviceId;
+
// 在服务端直接修改 Device.Secret(模拟运维修改密钥场景)
var device = Device.FindByCode(code)!;
var newSecret = Rand.NextString(16);
@@ -209,8 +186,7 @@ public class IoTZeroIntegrationTests : IClassFixture<IoTZeroWebFactory>
var onlines = DeviceOnline.FindAll(DeviceOnline._.DeviceId == deviceId);
Assert.NotEmpty(onlines);
- // 登录历史应有 2 条记录(第一次自动注册 + 本次)
- // WriteHistory 调用 SaveAsync,XCode EntityQueue ~1秒刷新,需轮询等待
+ // 登录历史应有 2 条记录(第一次自动注册 + 本次);EntityQueue 异步写入,需轮询等待
var logins = await WaitForDeviceHistory(deviceId, "Http登录", 2).ConfigureAwait(false);
Assert.True(logins.Count >= 2, $"应有至少 2 条登录历史,实际={logins.Count}");
@@ -219,10 +195,14 @@ public class IoTZeroIntegrationTests : IClassFixture<IoTZeroWebFactory>
#endregion
#region Step 5 — 心跳
- private async Task Step5_Ping(HttpDevice client, Int32 deviceId)
+ [Fact(DisplayName = "IoTZero_Step5_心跳")]
+ public async Task Step5_Ping()
{
XTrace.WriteLine("=== Step 5: 心跳 ===");
+ var client = _factory.TestClient;
+ var deviceId = _factory.TestDeviceId;
+
// 记录心跳前的 Pings(DeviceOnline 的 AdditionalFields 有 Pings 累加字段)
var onlineBefore = DeviceOnline.Find(DeviceOnline._.DeviceId == deviceId);
var pingsBefore = onlineBefore?.Pings ?? 0;
@@ -244,10 +224,13 @@ public class IoTZeroIntegrationTests : IClassFixture<IoTZeroWebFactory>
#endregion
#region Step 6 — WebSocket 建立确认
- private async Task Step6_WaitWebSocket(Int32 deviceId)
+ [Fact(DisplayName = "IoTZero_Step6_WebSocket建立确认")]
+ public async Task Step6_WaitWebSocket()
{
XTrace.WriteLine("=== Step 6: 等待 WebSocket 建立 ===");
+ var deviceId = _factory.TestDeviceId;
+
// HttpDevice 登录后会自动建立 WebSocket 通知连接(Features 包含 Notify)
// 轮询最长 5 秒等待 DeviceOnline.WebSocket = true
var deadline = DateTime.Now.AddSeconds(5);
@@ -267,10 +250,14 @@ public class IoTZeroIntegrationTests : IClassFixture<IoTZeroWebFactory>
#endregion
#region Step 7 — SendCommand 投递
- private async Task Step7_SendCommandAndReceive(HttpDevice client, String code)
+ [Fact(DisplayName = "IoTZero_Step7_SendCommand投递")]
+ public async Task Step7_SendCommandAndReceive()
{
XTrace.WriteLine("=== Step 7: SendCommand 投递 ===");
+ var client = _factory.TestClient;
+ var code = _factory.TestCode;
+
var tcs = new TaskCompletionSource<CommandEventArgs>(TaskCreationOptions.RunContinuationsAsynchronously);
client.Received += (_, e) =>
@@ -311,10 +298,13 @@ public class IoTZeroIntegrationTests : IClassFixture<IoTZeroWebFactory>
#endregion
#region Step 8 — 升级检查
- private async Task Step8_Upgrade(HttpDevice client)
+ [Fact(DisplayName = "IoTZero_Step8_升级检查")]
+ public async Task Step8_Upgrade()
{
XTrace.WriteLine("=== Step 8: 升级检查 ===");
+ var client = _factory.TestClient;
+
// 无新版本时应返回 null,不应抛出异常
IUpgradeInfo? info = null;
var ex = await Record.ExceptionAsync(async () =>
@@ -329,10 +319,13 @@ public class IoTZeroIntegrationTests : IClassFixture<IoTZeroWebFactory>
#endregion
#region Step 9 — 事件上报
- private async Task Step9_PostEvents(HttpDevice client)
+ [Fact(DisplayName = "IoTZero_Step9_事件上报")]
+ public async Task Step9_PostEvents()
{
XTrace.WriteLine("=== Step 9: 事件上报 ===");
+ var client = _factory.TestClient;
+
var events = new[]
{
new EventModel { Name = "TestEvent1", Type = "info", Remark = "IoTZero 集成测试事件1" },
@@ -365,4 +358,4 @@ public class IoTZeroIntegrationTests : IClassFixture<IoTZeroWebFactory>
return result;
}
#endregion
-}
+}
\ No newline at end of file
diff --git a/XUnitTest.Samples/IoTZero/IoTZeroWebFactory.cs b/XUnitTest.Samples/IoTZero/IoTZeroWebFactory.cs
index 0b77aec..331ca6f 100644
--- a/XUnitTest.Samples/IoTZero/IoTZeroWebFactory.cs
+++ b/XUnitTest.Samples/IoTZero/IoTZeroWebFactory.cs
@@ -1,6 +1,7 @@
extern alias IoTZero;
using IoTZero::IoT.Data;
+using IoTZero::IoTEdge;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
@@ -22,6 +23,18 @@ public sealed class IoTZeroWebFactory : WebApplicationFactory<IoTZero::Program>,
/// <summary>原始工作目录(析构时恢复)</summary>
private String _origCurrentDir = null!;
+
+ /// <summary>共享测试状态:已登录的客户端(贯穿全部 9 个测试步骤)</summary>
+ public HttpDevice TestClient { get; private set; } = null!;
+
+ /// <summary>共享测试状态:客户端设置(DeviceCode/DeviceSecret 在 Step1 登录后回填)</summary>
+ public ClientSetting TestSetting { get; private set; } = null!;
+
+ /// <summary>共享测试状态:设备编码(Step1 登录后填充,后续步骤使用)</summary>
+ public String TestCode { get; set; } = null!;
+
+ /// <summary>共享测试状态:设备 ID(Step2 验证实体后填充,后续步骤使用)</summary>
+ public Int32 TestDeviceId { get; set; }
#endregion
#region WebApplicationFactory 重写
@@ -77,11 +90,15 @@ public sealed class IoTZeroWebFactory : WebApplicationFactory<IoTZero::Program>,
CleanTestData();
// 预置测试所需的 Product(EdgeGateway),否则 OnRegister 会抛出“无效产品”
SeedProduct();
+ // 初始化共享测试状态:创建客户端(Step1 时以空 DeviceCode/DeviceSecret 触发自动注册)
+ TestSetting = new ClientSetting { Server = BaseUrl };
+ TestClient = new HttpDevice(TestSetting) { Log = XTrace.Log };
await Task.CompletedTask;
}
async Task IAsyncLifetime.DisposeAsync()
{
+ (TestClient as IDisposable)?.Dispose();
Dispose();
await Task.CompletedTask;
}
diff --git a/XUnitTest.Samples/ZeroServer/ZeroServerIntegrationTests.cs b/XUnitTest.Samples/ZeroServer/ZeroServerIntegrationTests.cs
index 6914136..06b3709 100644
--- a/XUnitTest.Samples/ZeroServer/ZeroServerIntegrationTests.cs
+++ b/XUnitTest.Samples/ZeroServer/ZeroServerIntegrationTests.cs
@@ -15,8 +15,9 @@ namespace XUnitTest.Samples.ZeroServer;
/// <summary>
/// ZeroServer HTTP 全链路集成测试。
/// 使用 WebApplicationFactory 启动真实 Kestrel(端口自动分配),NodeClient 通过 HTTP 连接。
-/// 单个测试方法串行执行 9 步,覆盖:自动注册登录、配置持久化、服务端实体验证、注销/重登、
-/// 心跳计数、WebSocket 通知建立、SendCommand + CommandReply、升级检查、事件上报。
+/// 9 个有序 [Fact] 方法(由 DefaultOrderer 按源码顺序执行),共用 ZeroServerWebFactory 持有的客户端状态。
+/// 覆盖:自动注册登录、配置持久化、服务端实体验证、注销/重登、心跳计数、
+/// WebSocket 通知建立、SendCommand + CommandReply、升级检查、事件上报。
/// </summary>
[Collection("SamplesIntegration")]
[TestCaseOrderer("NewLife.UnitTest.DefaultOrderer", "NewLife.UnitTest")]
@@ -26,11 +27,11 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
public ZeroServerIntegrationTests(ZeroServerWebFactory factory) => _factory = factory;
- #region 9 步串行集成测试
- [Fact(DisplayName = "ZeroServer_9步全链路集成测试")]
- public async Task FullIntegrationFlow_AllNineSteps()
+ #region Step 1 — 自动注册登录
+ [Fact(DisplayName = "ZeroServer_Step1_自动注册登录")]
+ public async Task Step1_AutoRegisterLogin()
{
- // 确保 Factory 已启动(首次访问时触发 ConfigureWebHost + CreateHost)
+ // 确保 Factory 已启动
Assert.False(_factory.BaseUrl.IsNullOrEmpty(), "BaseUrl 未初始化,Factory 启动失败");
XTrace.WriteLine("ZeroServer 测试服务地址:{0}", _factory.BaseUrl);
@@ -39,51 +40,14 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
NodeOnline.Delete("1=1");
NodeHistory.Delete("1=1");
- // 创建新的 ClientSetting(空 Code/Secret,触发服务端自动注册)
- var setting = new ClientSetting { Server = _factory.BaseUrl };
-
- using var client = new NodeClient(setting);
- client.Log = XTrace.Log;
-
- // Step 1 - 自动注册登录
- var (code, secret) = await Step1_AutoRegisterLogin(client, setting);
-
- // Step 2 - 服务端实体验证(通过 XCode 直接查询,同进程内共享 DAL 连接)
- var nodeId = await Step2_VerifyServerEntities(code);
-
- // Step 3 - 注销
- await Step3_Logout(client, nodeId);
-
- // Step 4 - 修改密钥后重登
- await Step4_ReLoginWithNewSecret(client, setting, code, nodeId);
-
- // Step 5 - 心跳
- await Step5_Ping(client, nodeId);
-
- // Step 6 - WebSocket 建立确认
- await Step6_WaitWebSocket(nodeId);
-
- // Step 7 - SendCommand + CommandReply
- await Step7_SendCommandAndReply(client, code);
-
- // Step 8 - 升级检查
- await Step8_Upgrade(client);
-
- // Step 9 - 事件上报
- await Step9_PostEvents(client);
- }
- #endregion
-
- #region Step 1 — 自动注册登录
- private async Task<(String code, String secret)> Step1_AutoRegisterLogin(NodeClient client, ClientSetting setting)
- {
- XTrace.WriteLine("=== Step 1: 自动注册登录 ===");
+ var client = _factory.TestClient;
+ var setting = _factory.TestSetting;
// 确保以空 Code/Secret 开始,触发服务端自动注册
setting.Code = null!;
setting.Secret = null!;
- await client.Login(null, CancellationToken.None).ConfigureAwait(false);
+ await client.Login(null, CancellationToken.None);
// 客户端状态验证
Assert.True(client.Logined, "Login 后 Logined 应为 true");
@@ -95,7 +59,7 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
XTrace.WriteLine("自动注册成功,Code={0}", setting.Code);
// 等待 ClientSetting.Save() 写文件(异步 IO,稍作等待)
- await Task.Delay(300).ConfigureAwait(false);
+ await Task.Delay(300);
// 配置文件持久化验证
var configContent = _factory.ReadClientConfigFile();
@@ -105,17 +69,21 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
XTrace.WriteLine("配置文件已写入,内容长度={0}", configContent!.Length);
- return (setting.Code, setting.Secret);
+ // 保存 Code 到 Factory,供后续步骤使用
+ _factory.TestCode = setting.Code;
}
#endregion
#region Step 2 — 服务端实体验证
- private async Task<Int32> Step2_VerifyServerEntities(String code)
+ [Fact(DisplayName = "ZeroServer_Step2_验证服务端实体")]
+ public async Task Step2_VerifyServerEntities()
{
XTrace.WriteLine("=== Step 2: 验证服务端实体 ===");
- // 等待服务端异步写库完成(Node 实体同步写入,短暂等待即可)
- await Task.Delay(300).ConfigureAwait(false);
+ var code = _factory.TestCode;
+
+ // 等待服务端异步写库完成
+ await Task.Delay(300);
// 验证 Node 实体已创建
var node = Node.FindByCodeWithCache(code, false);
@@ -139,20 +107,25 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
XTrace.WriteLine("NodeHistory 登录记录={0}", logins.Count);
- return node.Id;
+ // 保存 NodeId 到 Factory,供后续步骤使用
+ _factory.TestNodeId = node.Id;
}
#endregion
#region Step 3 — 注销
- private async Task Step3_Logout(NodeClient client, Int32 nodeId)
+ [Fact(DisplayName = "ZeroServer_Step3_注销")]
+ public async Task Step3_Logout()
{
XTrace.WriteLine("=== Step 3: 注销 ===");
- await client.Logout("集成测试注销", CancellationToken.None).ConfigureAwait(false);
+ var client = _factory.TestClient;
+ var nodeId = _factory.TestNodeId;
+
+ await client.Logout("集成测试注销", CancellationToken.None);
Assert.False(client.Logined, "Logout 后 Logined 应为 false");
- // 轮询等待 NodeOnline 被删除(RemoveOnline 同步执行,但直接 SQL 读取可能有短暂延迟)
+ // 轮询等待 NodeOnline 被删除
var emptyOnlines = await WaitForNodeOnlineEmpty(nodeId);
Assert.Empty(emptyOnlines);
@@ -165,10 +138,16 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
#endregion
#region Step 4 — 修改密钥后重登
- private async Task Step4_ReLoginWithNewSecret(NodeClient client, ClientSetting setting, String code, Int32 nodeId)
+ [Fact(DisplayName = "ZeroServer_Step4_修改密钥后重登")]
+ public async Task Step4_ReLoginWithNewSecret()
{
XTrace.WriteLine("=== Step 4: 修改密钥后重登 ===");
+ var client = _factory.TestClient;
+ var setting = _factory.TestSetting;
+ var code = _factory.TestCode;
+ var nodeId = _factory.TestNodeId;
+
// 在服务端直接修改 Node.Secret(模拟运维修改密钥场景)
var node = Node.FindByCodeWithCache(code, false)!;
var newSecret = Rand.NextString(16);
@@ -179,7 +158,7 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
setting.Secret = newSecret;
// 重新登录
- await client.Login("重登测试", CancellationToken.None).ConfigureAwait(false);
+ await client.Login("重登测试", CancellationToken.None);
Assert.True(client.Logined, "修改密钥后重登,Logined 应为 true");
@@ -196,19 +175,23 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
#endregion
#region Step 5 — 心跳
- private async Task Step5_Ping(NodeClient client, Int32 nodeId)
+ [Fact(DisplayName = "ZeroServer_Step5_心跳")]
+ public async Task Step5_Ping()
{
XTrace.WriteLine("=== Step 5: 心跳 ===");
+ var client = _factory.TestClient;
+ var nodeId = _factory.TestNodeId;
+
// 记录心跳前的 PingCount
- var onlineBefore = NodeOnline.FindByNodeId(nodeId);
+ var onlineBefore = NodeOnline.FindByNodeId(nodeId);
var pingCountBefore = onlineBefore?.PingCount ?? 0;
- var rs = await client.Ping(CancellationToken.None).ConfigureAwait(false);
+ var rs = await client.Ping(CancellationToken.None);
Assert.NotNull(rs);
// 等待服务端写库
- await Task.Delay(300).ConfigureAwait(false);
+ await Task.Delay(300);
// 刷新实体缓存后验证 PingCount 增加
var onlineAfter = NodeOnline.Find(NodeOnline._.NodeId == nodeId);
@@ -220,12 +203,15 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
}
#endregion
- #region Step 6 — WebSocket 通知建立
- private async Task Step6_WaitWebSocket(Int32 nodeId)
+ #region Step 6 — WebSocket 建立确认
+ [Fact(DisplayName = "ZeroServer_Step6_WebSocket建立确认")]
+ public async Task Step6_WaitWebSocket()
{
XTrace.WriteLine("=== Step 6: 等待 WebSocket 建立 ===");
- // NodeClient 登录后会自动建立 WebSocket 通知连接(因为 Features 包含 Notify)
+ var nodeId = _factory.TestNodeId;
+
+ // NodeClient 登录后会自动建立 WebSocket 通知连接
// 轮询最长 5 秒等待 NodeOnline.WebSocket = true
var deadline = DateTime.Now.AddSeconds(5);
NodeOnline? online = null;
@@ -233,7 +219,7 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
{
online = NodeOnline.Find(NodeOnline._.NodeId == nodeId);
if (online?.WebSocket == true) break;
- await Task.Delay(200).ConfigureAwait(false);
+ await Task.Delay(200);
}
Assert.NotNull(online);
@@ -244,21 +230,22 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
#endregion
#region Step 7 — SendCommand + CommandReply
- private async Task Step7_SendCommandAndReply(NodeClient client, String code)
+ [Fact(DisplayName = "ZeroServer_Step7_SendCommand与CommandReply")]
+ public async Task Step7_SendCommandAndReply()
{
XTrace.WriteLine("=== Step 7: SendCommand + CommandReply ===");
+ var client = _factory.TestClient;
+ var code = _factory.TestCode;
+
var tcs = new TaskCompletionSource<CommandEventArgs>(TaskCreationOptions.RunContinuationsAsynchronously);
client.Received += (_, e) =>
{
- // 只接受我们发出的命令
if (e.Model?.Command == "test:echo")
tcs.TrySetResult(e);
};
- // 通过 HttpClient 调用 [AllowAnonymous] 的 Node/SendCommand 接口
- // SendCommand 在 Service 层使用应用令牌验证,此处用已登录的设备令牌代替(测试环境 TokenService 不区分令牌来源)
using var http = new HttpClient();
var deviceToken = ((NewLife.Remoting.IApiClient)client).Token;
if (!deviceToken.IsNullOrEmpty())
@@ -268,16 +255,15 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
Code = code,
Command = "test:echo",
Argument = "hello",
- Timeout = 10, // 等待响应最多 10 秒
+ Timeout = 10,
});
var content = new StringContent(payload, System.Text.Encoding.UTF8, "application/json");
- var response = await http.PostAsync($"{_factory.BaseUrl}/Node/SendCommand", content).ConfigureAwait(false);
+ var response = await http.PostAsync($"{_factory.BaseUrl}/Node/SendCommand", content);
XTrace.WriteLine("SendCommand HTTP 状态={0}", response.StatusCode);
- // 等待客户端收到命令(最多 10 秒)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
- var cmdEvt = await tcs.Task.WaitAsync(cts.Token).ConfigureAwait(false);
+ var cmdEvt = await tcs.Task.WaitAsync(cts.Token);
Assert.NotNull(cmdEvt.Model);
Assert.Equal("test:echo", cmdEvt.Model!.Command);
@@ -288,23 +274,45 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
#endregion
#region Step 8 — 升级检查
- private async Task Step8_Upgrade(NodeClient client)
+ [Fact(DisplayName = "ZeroServer_Step8_升级检查")]
+ public async Task Step8_Upgrade()
{
XTrace.WriteLine("=== Step 8: 升级检查 ===");
- // 无新版本时应返回 null,不应抛出异常
+ var client = _factory.TestClient;
+
IUpgradeInfo? info = null;
var ex = await Record.ExceptionAsync(async () =>
{
- info = await client.Upgrade(null, CancellationToken.None).ConfigureAwait(false);
+ info = await client.Upgrade(null, CancellationToken.None);
});
Assert.Null(ex);
- // info 为 null 表示无可用升级,合法
XTrace.WriteLine("升级检查成功,info={0}", info == null ? "null(无升级)" : info.Version);
}
#endregion
+ #region Step 9 — 事件上报
+ [Fact(DisplayName = "ZeroServer_Step9_事件上报")]
+ public async Task Step9_PostEvents()
+ {
+ XTrace.WriteLine("=== Step 9: 事件上报 ===");
+
+ var client = _factory.TestClient;
+
+ var events = new[]
+ {
+ new EventModel { Name = "TestEvent1", Type = "info", Remark = "集成测试事件1" },
+ new EventModel { Name = "TestEvent2", Type = "alert", Remark = "集成测试事件2" },
+ };
+
+ var count = await client.PostEvents(events);
+
+ Assert.Equal(2, count);
+ XTrace.WriteLine("事件上报成功,返回数量={0}", count);
+ }
+ #endregion
+
#region 辅助方法
/// <summary>轮询等待 NodeHistory 出现指定条数记录(绕过实体缓存,直接 SQL 查询)</summary>
/// <param name="nodeId">节点 ID</param>
@@ -316,10 +324,9 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
var deadline = DateTime.Now.AddSeconds(5);
while (DateTime.Now < deadline)
{
- // 直接 SQL,绕过 EntityListCache(EntityQueue 异步写入,缓存尚未更新)
var list = NodeHistory.FindAll(NodeHistory._.NodeId == nodeId & NodeHistory._.Action == action);
if (list.Count >= minCount) return list;
- await Task.Delay(200).ConfigureAwait(false);
+ await Task.Delay(200);
}
return NodeHistory.FindAll(NodeHistory._.NodeId == nodeId & NodeHistory._.Action == action);
}
@@ -334,27 +341,9 @@ public class ZeroServerIntegrationTests : IClassFixture<ZeroServerWebFactory>
{
var list = NodeOnline.FindAll(NodeOnline._.NodeId == nodeId);
if (list.Count == 0) return list;
- await Task.Delay(200).ConfigureAwait(false);
+ await Task.Delay(200);
}
return NodeOnline.FindAll(NodeOnline._.NodeId == nodeId);
}
#endregion
-
- #region Step 9 — 事件上报
- private async Task Step9_PostEvents(NodeClient client)
- {
- XTrace.WriteLine("=== Step 9: 事件上报 ===");
-
- var events = new[]
- {
- new EventModel { Name = "TestEvent1", Type = "info", Remark = "集成测试事件1" },
- new EventModel { Name = "TestEvent2", Type = "alert", Remark = "集成测试事件2" },
- };
-
- var count = await client.PostEvents(events).ConfigureAwait(false);
-
- Assert.Equal(2, count);
- XTrace.WriteLine("事件上报成功,返回数量={0}", count);
- }
- #endregion
}
diff --git a/XUnitTest.Samples/ZeroServer/ZeroServerWebFactory.cs b/XUnitTest.Samples/ZeroServer/ZeroServerWebFactory.cs
index f089d47..d578623 100644
--- a/XUnitTest.Samples/ZeroServer/ZeroServerWebFactory.cs
+++ b/XUnitTest.Samples/ZeroServer/ZeroServerWebFactory.cs
@@ -6,6 +6,7 @@ using Microsoft.Extensions.Configuration;
using NewLife;
using NewLife.Log;
using Xunit;
+using ZeroServer::ZeroClient;
namespace XUnitTest.Samples.ZeroServer;
@@ -21,6 +22,18 @@ public sealed class ZeroServerWebFactory : WebApplicationFactory<ZeroServer::Pro
/// <summary>原始工作目录(析构时恢复)</summary>
private String _origCurrentDir = null!;
+
+ /// <summary>共享测试状态:已登录的客户端(贯穿全部 9 个测试步骤)</summary>
+ public NodeClient TestClient { get; private set; } = null!;
+
+ /// <summary>共享测试状态:客户端设置(Code/Secret 在 Step1 登录后回填)</summary>
+ public ClientSetting TestSetting { get; private set; } = null!;
+
+ /// <summary>共享测试状态:设备编码(Step1 登录后填充,后续步骤使用)</summary>
+ public String TestCode { get; set; } = null!;
+
+ /// <summary>共享测试状态:节点 ID(Step2 验证实体后填充,后续步骤使用)</summary>
+ public Int32 TestNodeId { get; set; }
#endregion
#region WebApplicationFactory 重写
@@ -75,11 +88,17 @@ public sealed class ZeroServerWebFactory : WebApplicationFactory<ZeroServer::Pro
// WAF 在 CreateHost 后调用 TryExtractHostAddress,自动将实际端口写入 ClientOptions.BaseAddress
BaseUrl = ClientOptions.BaseAddress?.ToString()?.TrimEnd('/') ?? "";
XTrace.WriteLine("ZeroServerWebFactory 启动,地址:{0}", BaseUrl);
+
+ // 初始化共享测试状态:创建客户端(Step1 时以空 Code/Secret 触发自动注册)
+ TestSetting = new ClientSetting { Server = BaseUrl };
+ TestClient = new NodeClient(TestSetting) { Log = XTrace.Log };
+
await Task.CompletedTask;
}
async Task IAsyncLifetime.DisposeAsync()
{
+ (TestClient as IDisposable)?.Dispose();
Dispose();
await Task.CompletedTask;
}