Java `Socket` 是介面,輕鬆 Mock;.NET `Socket`、`TcpClient` 皆為 `sealed class` 且方法非 `virtual`,Rhino Mocks、Moq、NSubstitute 均無法直接建立代理。本文提供三種實務解法,並比較優缺點。
// 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)。
缺點:需手寫包裝器、維護介面同步。
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+、屬於「最後手段」。
網路邏輯本質是「讀寫位元組流」。直接依賴 `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
.NET 核心庫設計早期未充分考慮可測試性,`Socket`/`TcpClient` 封閉性強。採用 **Wrapper + 介面** 是長期維護專案的最佳平衡;新專案建議直接依賴 `Stream`/`Pipe` 抽象,或使用 gRPC/HTTP Client 等高層級可測試 API。若被迫測試舊代碼,Microsoft Fakes 或整合測試為次選。