如何Mock VssConnection.GetClient<GitHttpClient>?包装类方案说明
场景与问题
我创建了一个使用MS GitHttpClient类读取Azure DevOps项目提交记录的应用,想要对逻辑进行单元测试,因此需要Mock VssConnection和GitHttpClient,但这两个类均未实现任何接口。
我可以Mock GitHttpClient,使其在调用GitHttpClient.GetCommitsAsync(...)时返回提交引用,但尝试MockVssConnection.GetClient<GitHttpClient>()时抛出以下异常:
Test method mycli.Tests.Unit.Services.GitServiceTests.TestVssConnectionMock threw exception: System.NotSupportedException: Unsupported expression: conn => conn.GetClient<GitHttpClient>() Non-overridable members (here: VssConnection.GetClient) may not be used in setup / verification expressions.
以下是我的测试类,第一个测试TestVssConnection因上述异常失败,第二个测试TestGitHttpClientMock可正常通过:
[TestClass] public class GitServiceTests { [TestMethod] public async Task TestVssConnectionMock() { var vssConnectionMock = new Mock<VssConnection>(new Uri("http://fake"), new VssCredentials()); var gitHttpClientMock = new Mock<GitHttpClient>(new Uri("http://fake"), new VssCredentials()); gitHttpClientMock.Setup(client => client.GetCommitsAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<GitQueryCommitsCriteria>(), null, null, null, It.IsAny<CancellationToken>())).Returns(Task.FromResult(new List<GitCommitRef> { new GitCommitRef { Comment = "abc" } })); vssConnectionMock.Setup(conn => conn.GetClient<GitHttpClient>()).Returns(gitHttpClientMock.Object); // EXCEPTION THROWN ABOVE ^ var gitHttpClient = vssConnectionMock.Object.GetClient<GitHttpClient>(); var commits = await gitHttpClient.GetCommitsAsync("", "", new GitQueryCommitsCriteria()); Assert.IsTrue(commits.Count == 1); } [TestMethod] public async Task TestGitHttpClientMock() { var gitHttpClientMock = new Mock<GitHttpClient>(new Uri("http://fake"), new VssCredentials()); gitHttpClientMock.Setup(client => client.GetCommitsAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<GitQueryCommitsCriteria>(), null, null, null, It.IsAny<CancellationToken>())).Returns(Task.FromResult(new List<GitCommitRef> { new GitCommitRef { Comment = "abc" } })); var commits = await gitHttpClientMock.Object.GetCommitsAsync("", "", new GitQueryCommitsCriteria()); Assert.IsTrue(commits.Count == 1); } }
我的问题是:如何MockVssConnection.GetClient<GitHttpClient>()使其返回我Mock的GitHttpClient?是否需要为VssConnection创建包装类?如果需要,最佳实现方式是什么?
我使用的是.NET 6、MsTest和MoQ。
解决方案
原因分析
MoQ只能Mock可重写的成员(虚方法、抽象方法或接口方法),而VssConnection.GetClient<T>是一个非虚方法,因此无法直接用MoQ对其进行Mock设置,这就是抛出异常的原因。
正确做法:创建包装类(适配器模式)
通过为VssConnection创建一个抽象接口和对应的包装类,将业务代码对VssConnection的依赖转换为对抽象接口的依赖,这样就能轻松Mock接口方法。
步骤1:定义抽象接口
创建一个包含GetClient<T>方法的接口,匹配VssConnection的对应方法签名:
public interface IVssConnectionWrapper { T GetClient<T>() where T : VssHttpClientBase; }
步骤2:实现包装类
创建包装类,内部封装真实的VssConnection实例,并实现接口方法:
public class VssConnectionWrapper : IVssConnectionWrapper { private readonly VssConnection _innerConnection; public VssConnectionWrapper(VssConnection innerConnection) { _innerConnection = innerConnection ?? throw new ArgumentNullException(nameof(innerConnection)); } public T GetClient<T>() where T : VssHttpClientBase { return _innerConnection.GetClient<T>(); } }
步骤3:修改业务代码依赖
在你的业务服务类中,将原来直接依赖VssConnection改为依赖IVssConnectionWrapper:
public class GitService { private readonly IVssConnectionWrapper _connectionWrapper; public GitService(IVssConnectionWrapper connectionWrapper) { _connectionWrapper = connectionWrapper ?? throw new ArgumentNullException(nameof(connectionWrapper)); } public async Task<List<GitCommitRef>> GetProjectCommitsAsync(string projectId, string repoId, GitQueryCommitsCriteria criteria) { var gitClient = _connectionWrapper.GetClient<GitHttpClient>(); return await gitClient.GetCommitsAsync(projectId, repoId, criteria, cancellationToken: default); } }
步骤4:编写单元测试
现在可以MockIVssConnectionWrapper,让它返回你预先Mock好的GitHttpClient:
[TestClass] public class GitServiceTests { [TestMethod] public async Task TestGetProjectCommits_ReturnsExpectedCommits() { // 1. Mock GitHttpClient var mockGitClient = new Mock<GitHttpClient>(new Uri("http://fake-azure-devops"), new VssCredentials()); mockGitClient.Setup(client => client.GetCommitsAsync( It.IsAny<string>(), It.IsAny<string>(), It.IsAny<GitQueryCommitsCriteria>(), null, null, null, It.IsAny<CancellationToken>())) .ReturnsAsync(new List<GitCommitRef> { new GitCommitRef { Comment = "test commit" } }); // 2. Mock IVssConnectionWrapper var mockConnectionWrapper = new Mock<IVssConnectionWrapper>(); mockConnectionWrapper.Setup(wrapper => wrapper.GetClient<GitHttpClient>()) .Returns(mockGitClient.Object); // 3. 实例化业务服务并执行测试 var gitService = new GitService(mockConnectionWrapper.Object); var result = await gitService.GetProjectCommitsAsync("", "", new GitQueryCommitsCriteria()); // 4. 断言结果 Assert.AreEqual(1, result.Count); Assert.AreEqual("test commit", result[0].Comment); } [TestMethod] public async Task TestGitHttpClientMock() { var gitHttpClientMock = new Mock<GitHttpClient>(new Uri("http://fake"), new VssCredentials()); gitHttpClientMock.Setup(client => client.GetCommitsAsync( It.IsAny<string>(), It.IsAny<string>(), It.IsAny<GitQueryCommitsCriteria>(), null, null, null, It.IsAny<CancellationToken>())) .Returns(Task.FromResult(new List<GitCommitRef> { new GitCommitRef { Comment = "abc" } })); var commits = await gitHttpClientMock.Object.GetCommitsAsync("", "", new GitQueryCommitsCriteria()); Assert.IsTrue(commits.Count == 1); } }
为什么这是最佳实践
- 遵循依赖倒置原则:业务代码依赖抽象(接口)而非具体实现(VssConnection),降低耦合度。
- 保持包装类的简单性:仅封装必要的方法,不添加额外逻辑,确保和原
VssConnection的行为一致。 - 测试友好:通过Mock抽象接口,完全隔离外部依赖,专注于业务逻辑的验证。
内容的提问来源于stack exchange,提问作者Simon K

