You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何Mock VssConnection.GetClient<GitHttpClient>?包装类方案说明

问题:Mock VssConnection.GetClient()失败的解决方案

场景与问题

我创建了一个使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 06:25:40