集成测试增强及ApiHttpClient参数优化 修改InvokeAsync方法使cancellationToken为可选参数。新增EchoServer、HttpServer、NetworkServer三类集成测试,覆盖TCP/UDP/HTTP/WebSocket等场景,提升网络服务端功能的自动化验证能力。石头 authored at 2026-04-27 17:46:45
diff --git a/NewLife.Core/Remoting/ApiHttpClient.cs b/NewLife.Core/Remoting/ApiHttpClient.cs
index 728023b..d045a4e 100644
--- a/NewLife.Core/Remoting/ApiHttpClient.cs
+++ b/NewLife.Core/Remoting/ApiHttpClient.cs
@@ -390,7 +390,7 @@ public partial class ApiHttpClient : DisposeBase, IApiClient, IConfigMapping, IL
/// <param name="args">参数</param>
/// <param name="cancellationToken">取消通知</param>
/// <returns></returns>
- public Task<TResult?> InvokeAsync<TResult>(String action, Object? args, CancellationToken cancellationToken)
+ public Task<TResult?> InvokeAsync<TResult>(String action, Object? args, CancellationToken cancellationToken = default)
{
var method = HttpMethod.Post;
#if NETCOREAPP || NETSTANDARD2_1
diff --git a/XUnitTest.Core/Integration/EchoServerIntegrationTests.cs b/XUnitTest.Core/Integration/EchoServerIntegrationTests.cs
new file mode 100644
index 0000000..7575cc1
--- /dev/null
+++ b/XUnitTest.Core/Integration/EchoServerIntegrationTests.cs
@@ -0,0 +1,136 @@
+using NewLife;
+using NewLife.Data;
+using NewLife.Log;
+using NewLife.Net;
+using Xunit;
+
+namespace XUnitTest.Integration;
+
+/// <summary>EchoServer 集成测试固定装置,复现 Samples/Zero.EchoServer 的逻辑</summary>
+public class EchoServerFixture : IDisposable
+{
+ /// <summary>回声服务端实例</summary>
+ public NetServer Server { get; }
+
+ public EchoServerFixture()
+ {
+ XTrace.UseConsole();
+
+ var server = new EchoNetServer
+ {
+ Port = 0,
+ Log = XTrace.Log,
+#if DEBUG
+ SessionLog = XTrace.Log,
+#endif
+ };
+ Server = server;
+ Server.Start();
+ }
+
+ public void Dispose() => Server?.Stop("IntegrationTestDone");
+}
+
+/// <summary>定义回声服务端,把收到的数据原样发回去,用于网络性能压测</summary>
+class EchoNetServer : NetServer<EchoSession>
+{
+}
+
+/// <summary>回声会话,收到数据后原样返回</summary>
+class EchoSession : NetSession<EchoNetServer>
+{
+ /// <summary>收到客户端数据</summary>
+ /// <param name="e"></param>
+ protected override void OnReceive(ReceivedEventArgs e)
+ {
+ var packet = e.Packet;
+ if (packet == null || packet.Length == 0) return;
+
+ // 把收到的数据发回去
+ Send(packet);
+ }
+}
+
+/// <summary>EchoServer 集成测试,验证 TCP/UDP 回显功能</summary>
+[TestCaseOrderer("NewLife.UnitTest.DefaultOrderer", "NewLife.UnitTest")]
+public class EchoServerIntegrationTests : IClassFixture<EchoServerFixture>
+{
+ private readonly EchoServerFixture _fixture;
+
+ public EchoServerIntegrationTests(EchoServerFixture fixture) => _fixture = fixture;
+
+ [Fact(DisplayName = "01-服务端已启动且端口已分配")]
+ public void Test01_ServerStarted()
+ {
+ Assert.True(_fixture.Server.Active, "服务端应处于运行状态");
+ Assert.True(_fixture.Server.Port > 0, "端口应已分配");
+
+ XTrace.WriteLine("EchoServer 已在端口 {0} 上启动", _fixture.Server.Port);
+ }
+
+ [Fact(DisplayName = "02-TCP 回显 16 字节小包")]
+ public async Task Test02_TcpEcho16Bytes()
+ {
+ var port = _fixture.Server.Port;
+ var client = new NetUri($"tcp://127.0.0.1:{port}").CreateRemote();
+
+ var payload = new Byte[16];
+ Random.Shared.NextBytes(payload);
+
+ var wait = new TaskCompletionSource<Byte[]>();
+ client.Received += (s, e) => wait.TrySetResult(e.GetBytes());
+
+ client.Open();
+ client.Send(payload);
+
+ var received = await wait.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ Assert.Equal(payload, received);
+
+ client.Close("Test02Done");
+ }
+
+ [Fact(DisplayName = "03-TCP 回显 1024 字节大包")]
+ public async Task Test03_TcpEcho1024Bytes()
+ {
+ var port = _fixture.Server.Port;
+ var client = new NetUri($"tcp://127.0.0.1:{port}").CreateRemote();
+
+ var payload = new Byte[1024];
+ Random.Shared.NextBytes(payload);
+
+ var wait = new TaskCompletionSource<Byte[]>();
+ client.Received += (s, e) => wait.TrySetResult(e.GetBytes());
+
+ client.Open();
+ client.Send(payload);
+
+ var received = await wait.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ Assert.Equal(payload, received);
+
+ client.Close("Test03Done");
+ }
+
+ [Fact(DisplayName = "04-UDP 回显消息")]
+ public async Task Test04_UdpEcho()
+ {
+ var port = _fixture.Server.Port;
+ var client = new NetUri($"udp://127.0.0.1:{port}").CreateRemote();
+
+ var payload = new Byte[32];
+ Random.Shared.NextBytes(payload);
+
+ var wait = new TaskCompletionSource<Byte[]>();
+ client.Received += (s, e) => wait.TrySetResult(e.GetBytes());
+
+ client.Open();
+ client.Send(payload);
+
+ var received = await wait.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ Assert.Equal(payload, received);
+
+ client.Close("Test04Done");
+ }
+}
diff --git a/XUnitTest.Core/Integration/HttpServerIntegrationTests.cs b/XUnitTest.Core/Integration/HttpServerIntegrationTests.cs
new file mode 100644
index 0000000..7b25552
--- /dev/null
+++ b/XUnitTest.Core/Integration/HttpServerIntegrationTests.cs
@@ -0,0 +1,165 @@
+using System.Net.WebSockets;
+using System.Text;
+using NewLife;
+using NewLife.Data;
+using NewLife.Http;
+using NewLife.Log;
+using NewLife.Remoting;
+using Xunit;
+
+namespace XUnitTest.Integration;
+
+/// <summary>HttpServer 集成测试固定装置,复现 Samples/Zero.HttpServer 的路由配置</summary>
+public class HttpServerFixture : IDisposable
+{
+ /// <summary>HTTP 服务端实例</summary>
+ public HttpServer Server { get; }
+
+ /// <summary>服务端基础地址</summary>
+ public Uri BaseUri { get; }
+
+ public HttpServerFixture()
+ {
+ XTrace.UseConsole();
+
+ var server = new HttpServer
+ {
+ Name = "集成测试Http服务器",
+ Port = 0,
+ Log = XTrace.Log,
+#if DEBUG
+ SessionLog = XTrace.Log,
+#endif
+ };
+
+ // 简单路径,返回字符串
+ server.Map("/", () => "<h1>Hello NewLife!</h1></br> " + DateTime.Now.ToFullString());
+ server.Map("/user", (String act, Int32 uid) => new { code = 0, data = $"User.{act}({uid}) success!" });
+
+ // 自定义处理器,操作 Http 上下文
+ server.Map("/my", new IntegrationHttpHandler());
+
+ // 自定义控制器
+ server.MapController<ApiController>("/api");
+
+ // WebSocket 处理器
+ server.Map("/ws", new WebSocketHandler());
+
+ server.Start();
+
+ Server = server;
+ BaseUri = new Uri($"http://127.0.0.1:{server.Port}");
+ }
+
+ public void Dispose() => Server?.Dispose();
+}
+
+/// <summary>内联 HttpHandler,对应 Samples/Zero.HttpServer/MyHttpHandler.cs 的核心逻辑</summary>
+class IntegrationHttpHandler : IHttpHandler
+{
+ /// <summary>处理请求</summary>
+ /// <param name="context">Http 上下文</param>
+ public void ProcessRequest(IHttpContext context)
+ {
+ var name = context.Parameters["name"];
+ var html = $"<h2>你好,<span color=\"red\">{name}</span></h2>";
+ context.Response.SetResult(html);
+ }
+}
+
+/// <summary>HttpServer 集成测试,验证 HTTP/WebSocket 功能</summary>
+[TestCaseOrderer("NewLife.UnitTest.DefaultOrderer", "NewLife.UnitTest")]
+public class HttpServerIntegrationTests : IClassFixture<HttpServerFixture>
+{
+ private readonly HttpServerFixture _fixture;
+
+ public HttpServerIntegrationTests(HttpServerFixture fixture) => _fixture = fixture;
+
+ [Fact(DisplayName = "01-服务端已启动且端口已分配")]
+ public void Test01_ServerStarted()
+ {
+ Assert.True(_fixture.Server.Active, "服务端应处于运行状态");
+ Assert.True(_fixture.Server.Port > 0, "端口应已分配");
+
+ XTrace.WriteLine("HttpServer 已在端口 {0} 上启动", _fixture.Server.Port);
+ }
+
+ [Fact(DisplayName = "02-GET / 返回 Hello NewLife")]
+ public async Task Test02_HttpGetRoot()
+ {
+ using var client = new HttpClient { BaseAddress = _fixture.BaseUri };
+ var html = await client.GetStringAsync("/");
+
+ Assert.NotEmpty(html);
+ Assert.Contains("Hello NewLife", html);
+
+ XTrace.WriteLine("GET / 响应:{0}", html);
+ }
+
+ [Fact(DisplayName = "03-GET /user 返回正确的 API 结果")]
+ public async Task Test03_UserApiRequest()
+ {
+ var http = new ApiHttpClient(_fixture.BaseUri.ToString())
+ {
+ Log = XTrace.Log,
+ };
+
+ var rs = await http.GetAsync<String>("/user", new { act = "Delete", uid = 1234 });
+
+ Assert.Equal("User.Delete(1234) success!", rs);
+
+ XTrace.WriteLine("GET /user 响应:{0}", rs);
+ }
+
+ [Fact(DisplayName = "04-GET /my 自定义处理器返回正确响应")]
+ public async Task Test04_CustomHandler()
+ {
+ using var client = new HttpClient { BaseAddress = _fixture.BaseUri };
+ var html = await client.GetStringAsync("/my?name=stone");
+
+ Assert.Equal("<h2>你好,<span color=\"red\">stone</span></h2>", html);
+
+ XTrace.WriteLine("GET /my 响应:{0}", html);
+ }
+
+ [Fact(DisplayName = "05-GET /api/info 控制器返回机器信息")]
+ public async Task Test05_ApiInfoRequest()
+ {
+ var http = new ApiHttpClient(_fixture.BaseUri.ToString())
+ {
+ Log = XTrace.Log,
+ };
+
+ var rs = await http.GetAsync<Object>("/api/info", new { state = "test" });
+
+ Assert.NotNull(rs);
+
+ var json = NewLife.Serialization.JsonHelper.ToJson(rs);
+ Assert.Contains("MachineName", json);
+
+ XTrace.WriteLine("GET /api/info 响应:{0}", json);
+ }
+
+ [Fact(DisplayName = "06-WebSocket 连接收发消息")]
+ public async Task Test06_WebSocketConnect()
+ {
+ var wsUri = new Uri($"ws://127.0.0.1:{_fixture.Server.Port}/ws");
+ using var ws = new ClientWebSocket();
+
+ await ws.ConnectAsync(wsUri, default);
+ Assert.Equal(WebSocketState.Open, ws.State);
+
+ var msg = "Hello NewLife";
+ await ws.SendAsync(Encoding.UTF8.GetBytes(msg), System.Net.WebSockets.WebSocketMessageType.Text, true, default);
+
+ var buf = new Byte[1024];
+ var result = await ws.ReceiveAsync(buf, default);
+ var reply = Encoding.UTF8.GetString(buf, 0, result.Count);
+
+ // WebSocketHandler.SendAll 会把消息广播回来,格式:[remote]说,msg
+ Assert.Contains(msg, reply);
+ XTrace.WriteLine("WebSocket 收到:{0}", reply);
+
+ await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "测试完成", default);
+ }
+}
diff --git a/XUnitTest.Core/Integration/NetworkServerIntegrationTests.cs b/XUnitTest.Core/Integration/NetworkServerIntegrationTests.cs
new file mode 100644
index 0000000..d82e296
--- /dev/null
+++ b/XUnitTest.Core/Integration/NetworkServerIntegrationTests.cs
@@ -0,0 +1,210 @@
+using System.Net.Sockets;
+using System.Text;
+using NewLife;
+using NewLife.Data;
+using NewLife.Log;
+using NewLife.Model;
+using NewLife.Net;
+using Xunit;
+
+namespace XUnitTest.Integration;
+
+/// <summary>NetworkServer 集成测试固定装置,复现 Samples/Zero.Server 的逻辑</summary>
+public class NetworkServerFixture : IDisposable
+{
+ /// <summary>网络服务端实例</summary>
+ public NetServer Server { get; }
+
+ public NetworkServerFixture()
+ {
+ XTrace.UseConsole();
+
+ var server = new MyNetServer
+ {
+ Port = 0,
+ Log = XTrace.Log,
+#if DEBUG
+ SessionLog = XTrace.Log,
+#endif
+ };
+ Server = server;
+ Server.Start();
+ }
+
+ public void Dispose() => Server?.Stop("IntegrationTestDone");
+}
+
+/// <summary>定义服务端,用于管理所有网络会话,对应 Samples/Zero.Server/MyNetServer.cs</summary>
+class MyNetServer : NetServer<MyNetSession>
+{
+}
+
+/// <summary>定义会话。连接时发欢迎语,收到数据后返回反转字符串</summary>
+class MyNetSession : NetSession<MyNetServer>
+{
+ /// <summary>客户端连接</summary>
+ protected override void OnConnected()
+ {
+ // 发送欢迎语
+ Send($"Welcome to visit {Environment.MachineName}! [{Remote}]\r\n");
+
+ base.OnConnected();
+ }
+
+ /// <summary>客户端断开连接</summary>
+ /// <param name="reason">断开原因</param>
+ protected override void OnDisconnected(String reason)
+ {
+ WriteLog("客户端 {0} 已断开连接。{1}", Remote, reason);
+
+ base.OnDisconnected(reason);
+ }
+
+ /// <summary>收到客户端数据</summary>
+ /// <param name="e">接收事件参数</param>
+ protected override void OnReceive(ReceivedEventArgs e)
+ {
+ var packet = e.Packet;
+ if (packet == null || packet.Length == 0) return;
+
+ WriteLog("收到:{0}", packet.ToStr());
+
+ // 把收到的字符串反转后发回
+ Send(packet.ToStr().Reverse().Join(null));
+ }
+
+ /// <summary>出错</summary>
+ /// <param name="sender">事件发送者</param>
+ /// <param name="e">异常事件参数</param>
+ protected override void OnError(Object sender, ExceptionEventArgs e)
+ {
+ WriteLog("[{0}] 错误:{1}", e.Action, e.Exception?.GetTrue().Message);
+
+ base.OnError(sender, e);
+ }
+}
+
+/// <summary>NetworkServer 集成测试,验证 TCP/UDP 连接与收发功能</summary>
+[TestCaseOrderer("NewLife.UnitTest.DefaultOrderer", "NewLife.UnitTest")]
+public class NetworkServerIntegrationTests : IClassFixture<NetworkServerFixture>
+{
+ private readonly NetworkServerFixture _fixture;
+
+ public NetworkServerIntegrationTests(NetworkServerFixture fixture) => _fixture = fixture;
+
+ [Fact(DisplayName = "01-服务端已启动且端口已分配")]
+ public void Test01_ServerStarted()
+ {
+ Assert.True(_fixture.Server.Active, "服务端应处于运行状态");
+ Assert.True(_fixture.Server.Port > 0, "端口应已分配");
+
+ XTrace.WriteLine("NetworkServer 已在端口 {0} 上启动", _fixture.Server.Port);
+ }
+
+ [Fact(DisplayName = "02-TcpClient 连接后收到欢迎消息")]
+ public async Task Test02_TcpClientWelcome()
+ {
+ var port = _fixture.Server.Port;
+ using var client = new TcpClient();
+ await client.ConnectAsync("127.0.0.1", port);
+ var ns = client.GetStream();
+
+ // 服务端连接后主动发欢迎语
+ var buf = new Byte[1024];
+ ns.ReadTimeout = 5_000;
+ var count = await ns.ReadAsync(buf);
+ var welcome = Encoding.UTF8.GetString(buf, 0, count);
+
+ Assert.Contains("Welcome", welcome);
+ XTrace.WriteLine("<= {0}", welcome.Trim());
+ }
+
+ [Fact(DisplayName = "03-TcpClient 发送消息后收到反转字符串")]
+ public async Task Test03_TcpClientEcho()
+ {
+ var port = _fixture.Server.Port;
+ using var client = new TcpClient();
+ await client.ConnectAsync("127.0.0.1", port);
+ var ns = client.GetStream();
+
+ // 先收欢迎语
+ var buf = new Byte[1024];
+ ns.ReadTimeout = 5_000;
+ await ns.ReadAsync(buf);
+
+ // 发送数据
+ const String msg = "Hello NewLife";
+ var msgBytes = Encoding.UTF8.GetBytes(msg);
+ await ns.WriteAsync(msgBytes);
+
+ // 接收反转后的数据
+ var count = await ns.ReadAsync(buf);
+ var reply = Encoding.UTF8.GetString(buf, 0, count);
+
+ Assert.Equal("efiLweN olleH", reply);
+ XTrace.WriteLine("<= {0}", reply);
+ }
+
+ [Fact(DisplayName = "04-UdpClient 发包后收到欢迎消息和反转回显")]
+ public async Task Test04_UdpClientEcho()
+ {
+ var port = _fixture.Server.Port;
+ using var udp = new UdpClient();
+
+ var endpoint = new System.Net.IPEndPoint(System.Net.IPAddress.Loopback, port);
+ const String msg = "Hello NewLife";
+ var msgBytes = Encoding.UTF8.GetBytes(msg);
+
+ udp.Client.ReceiveTimeout = 5_000;
+
+ // 发送第一个包,服务端才建立 UDP 会话
+ await udp.SendAsync(msgBytes, msgBytes.Length, endpoint);
+
+ // 收到 welcome(OnConnected 触发)
+ var result1 = await udp.ReceiveAsync();
+ var welcome = Encoding.UTF8.GetString(result1.Buffer);
+ Assert.Contains("Welcome", welcome);
+ XTrace.WriteLine("<= {0}", welcome.Trim());
+
+ // 收到反转字符串(OnReceive 触发)
+ var result2 = await udp.ReceiveAsync();
+ var reply = Encoding.UTF8.GetString(result2.Buffer);
+ Assert.Equal("efiLweN olleH", reply);
+ XTrace.WriteLine("<= {0}", reply);
+
+ // 发空包通知服务端关闭 UDP 会话
+ await udp.SendAsync([], 0, endpoint);
+ }
+
+ [Fact(DisplayName = "05-ISocketClient(TCP) 完整收发流程")]
+ public async Task Test05_TcpSession()
+ {
+ var port = _fixture.Server.Port;
+ var uri = new NetUri($"tcp://127.0.0.1:{port}");
+ var client = uri.CreateRemote();
+ client.Name = "集成测试Tcp客户";
+ client.Log = XTrace.Log;
+
+ // 关闭异步模式,使用同步 ReceiveAsync 以便确定包边界
+ if (client is TcpSession tcp) tcp.MaxAsync = 0;
+
+ // 接收欢迎语(内部自动建立连接)
+ using var welcome = await client.ReceiveAsync(default).WaitAsync(TimeSpan.FromSeconds(5));
+ var welcomeStr = welcome.ToStr();
+ Assert.Contains("Welcome", welcomeStr);
+ XTrace.WriteLine("<= {0}", welcomeStr.Trim());
+
+ // 发送数据
+ const String msg = "Hello NewLife";
+ client.Send(msg);
+ XTrace.WriteLine("=> {0}", msg);
+
+ // 接收反转字符串
+ using var reply = await client.ReceiveAsync(default).WaitAsync(TimeSpan.FromSeconds(5));
+ var replyStr = reply.ToStr();
+ Assert.Equal("efiLweN olleH", replyStr);
+ XTrace.WriteLine("<= {0}", replyStr);
+
+ client.Close("Test05Done");
+ }
+}