首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> .NET 實例教學

.NET Socket TDD 難題:介面缺失下的包裝器模式與 Mock 策略

     編輯:.NET 實例教學

System.Net.Sockets.Socket/TcpClient 為密封類別,無法直接 Mock——如何在 TDD 中測試網路邏輯?

Java `Socket` 是介面,輕鬆 Mock;.NET `Socket`、`TcpClient` 皆為 `sealed class` 且方法非 `virtual`,Rhino Mocks、Moq、NSubstitute 均無法直接建立代理。本文提供三種實務解法,並比較優缺點。

方案一:自訂介面 + 包裝器 (Wrapper/Adapter) —— 推薦

// 1. 定義介面
public interface ITcpClient : IDisposable {
    bool Connected { get; }
    Stream GetStream();
    void Connect(string host, int port);
    void Connect(IPAddress address, int port);
    Task ConnectAsync(string host, int port);
    void Close();
}

// 2. 實作包裝器
public class TcpClientWrapper : ITcpClient {
    private readonly TcpClient _inner = new TcpClient();
    public bool Connected => _inner.Connected;
    public Stream GetStream() => _inner.GetStream();
    public void Connect(string host, int port) => _inner.Connect(host, port);
    public void Connect(IPAddress address, int port) => _inner.Connect(address, port);
    public Task ConnectAsync(string host, int port) => _inner.ConnectAsync(host, port);
    public void Close() => _inner.Close();
    public void Dispose() => _inner.Dispose();
}

// 3. 生產程式碼依賴介面
public class MyService {
    private readonly Func _clientFactory;
    public MyService(Func clientFactory) => _clientFactory = clientFactory;
    public async Task Send(string host, int port, byte[] data) {
        using (var client = _clientFactory()) {
            await client.ConnectAsync(host, port);
            await client.GetStream().WriteAsync(data, 0, data.Length);
        }
    }
}

// 4. 測試使用 Mock
[Test]
public async Task Send_WritesData() {
    var mockStream = new Mock();
    var mockClient = new Mock();
    mockClient.Setup(c => c.ConnectAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask);
    mockClient.Setup(c => c.GetStream()).Returns(mockStream.Object);
    mockClient.Setup(c => c.Connected).Returns(true);

    var service = new MyService(() => mockClient.Object);
    await service.Send("localhost", 1234, new byte[] { 1, 2, 3 });

    mockStream.Verify(s => s.WriteAsync(It.Is(b => b.SequenceEqual(new byte[] { 1, 2, 3 })), 0, 3, It.IsAny()), Times.Once);
}

優點:完全可測、符合 DIP、可替換實作(如 TLS、Unix Domain Socket、測試用 In-Memory Stream)。
缺點:需手寫包裝器、維護介面同步。

方案二:Microsoft Fakes (Shims) —— 僅限 Visual Studio Enterprise

using (ShimsContext.Create()) {
    System.Net.Fakes.ShimTcpClient.AllInstances.ConnectStringInt32 = (client, host, port) => { /* no-op */ };
    System.Net.Fakes.ShimTcpClient.AllInstances.GetStream = client => new MemoryStream();
    // 測試邏輯...
}

優點:無需修改生產程式碼、可攔截靜態/非虛擬/密封成員。
缺點:需 VS Enterprise、編譯較慢、不支援 .NET Core/.NET 5+、屬於「最後手段」。

方案三:整合測試替代單元測試

  • 啟動真實 `TcpListener` 在 `localhost` 隨機埠,測試完整網路堆疊。
  • 使用 `TestServer` (ASP.NET Core) 或 `NamedPipe` 模擬本機通訊。
  • 優點:真實度高、捕捉序列化、編碼、逾時、協定錯誤。缺點:執行慢、易碎、需隔離埠衝突。

進階:抽象 Stream 而非 Socket

網路邏輯本質是「讀寫位元組流」。直接依賴 `Stream` 介面:

public interface IStreamProvider {
    Task GetStreamAsync(CancellationToken ct = default);
}
public class TcpStreamProvider : IStreamProvider {
    public async Task GetStreamAsync(CancellationToken ct) {
        var client = new TcpClient();
        await client.ConnectAsync(_host, _port, ct);
        return client.GetStream();
    }
}

測試時注入 `MemoryStream` 或 `Mock`,完全跳過 Socket 層,測試更純粹、快速。

結語

.NET 核心庫設計早期未充分考慮可測試性,`Socket`/`TcpClient` 封閉性強。採用 **Wrapper + 介面** 是長期維護專案的最佳平衡;新專案建議直接依賴 `Stream`/`Pipe` 抽象,或使用 gRPC/HTTP Client 等高層級可測試 API。若被迫測試舊代碼,Microsoft Fakes 或整合測試為次選。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved