NewLife/X

集成测试增强及ApiHttpClient参数优化

修改InvokeAsync方法使cancellationToken为可选参数。新增EchoServer、HttpServer、NetworkServer三类集成测试,覆盖TCP/UDP/HTTP/WebSocket等场景,提升网络服务端功能的自动化验证能力。
石头 authored at 2026-04-27 17:46:45
95dfad4
Tree
1 Parent(s) 78fa61f
Summary: 4 changed files with 512 additions and 1 deletions.
Modified +1 -1
Added +136 -0
Added +165 -0
Added +210 -0
Modified +1 -1
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
Added +136 -0
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");
+    }
+}
Added +165 -0
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);
+    }
+}
Added +210 -0
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");
+    }
+}