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

.NET中JIRA服务单元测试:无法Mock Atlassian.Jira.Jira类

问题:Mock Atlassian.Jira的Jira类时遇到无参构造函数缺失错误

我开发了TcJiraService类,负责JIRA工单的获取与创建,基于Atlassian.Jira包的Jira客户端实现,业务代码运行正常,但编写单元测试时遇到问题:用Moq框架Mock Jira类时抛出System.ArgumentException,提示无法实例化代理,找不到无参构造函数。即使改用接口依赖注入,问题依然存在。之前用HttpClient连接JIRA时测试正常,现在求解决方法。

业务代码

public class TcJiraService(Jira tJiraClient, IOptions<TcJiraOptions> tOptions, ILogger<TcJiraService> tLogger)
{
    private readonly TcJiraOptions mOptions = tOptions.Value;

    public async Task<List<Issue>> GetJiraIssuesAsync()
    {
        try
        {
            var aJqlQuery = $"project = '{mOptions.ProjectKey}'";
            var aIssues = await tJiraClient.Issues.GetIssuesFromJqlAsync(aJqlQuery);
            foreach (var aIssue in aIssues)
            {
                tLogger.LogInformation("Issue Key: {aIssue.Key}, Summary: {aIssue.Summary}", aIssue.Key, aIssue.Summary);
            }
            return aIssues.ToList();
        }
        catch (Exception aEx)
        {
            tLogger.LogError("An error occurred while fetching issues from JIRA: {Message}", aEx.Message);
            return [];
        }
    }

    public async Task<string?> CreateJiraIssueAsync(string? tSummary, string? tDescription, string? tEmailSender)
    {
        try
        {
            var aIssue = tJiraClient.CreateIssue(mOptions.ProjectKey);
            aIssue.Type = new IssueType("9");
            aIssue.Priority = new IssuePriority("5");
            aIssue.Summary = tSummary;
            aIssue.Description = tDescription;
            await aIssue.SaveChangesAsync();
            return aIssue.Key.Value;
        }
        catch (Exception aEx)
        {
            tLogger.LogError("Failed to create JIRA issue: {Message}", aEx.Message);
            return null;
        }
    }

    public async Task<bool> AttachContentToIssueAsync(string tIssueKey, byte[] tContentData, string tFileName)
    {
        try
        {
            var aIssue = await tJiraClient.Issues.GetIssueAsync(tIssueKey);
            var aAttachment = new UploadAttachmentInfo(tFileName, tContentData);
            await aIssue.AddAttachmentAsync([aAttachment]);
            return true;
        }
        catch (Exception aEx)
        {
            tLogger.LogError("Failed to attach file to JIRA issue. {Message}", aEx.Message);
            return false;
        }
    }
}

测试代码

public class TcJiraServiceTest
{
    private readonly Mock<Jira> mockJiraClient = new Mock<Jira>();
    private readonly Mock<ILogger<TcJiraService>> mockLogger = new Mock<ILogger<TcJiraService>>();
    private readonly IOptions<TcJiraOptions> mockOptions = Options.Create(new TcJiraOptions
    {
        ProjectKey = "TEST-1",
        BaseUrl = "example.com"
    });

    private TcJiraService service;

    public TcJiraServiceTest()
    {
        this.service = new TcJiraService(mockJiraClient.Object, mockOptions, mockLogger.Object);
    }

    [Fact]
    public async Task CreateJiraIssueAsync_ReturnsIssueKey_WhenCalledWithValidParameters()
    {
        // Arrange
        var fakeIssue = new Mock<Issue>(mockJiraClient.Object, "TEST", "1"); 
        fakeIssue.Setup(issue => issue.Key).Returns("TEST-1");

        // Act
        var issueKey = await service.CreateJiraIssueAsync("Test Summary", "Test Description", "test@example.com");

        // Assert
        Assert.Equal("TEST-1", issueKey);
    }
    
}

错误信息

System.ArgumentException: Can not instantiate proxy of class: Atlassian.Jira.Jira.

System.ArgumentException
Can not instantiate proxy of class: Atlassian.Jira.Jira.
Could not find a parameterless constructor. (Parameter 'constructorArguments')
   at Castle.DynamicProxy.ProxyGenerator.CreateClassProxyInstance(Type proxyType, List`1 proxyArguments, Type classToProxy, Object[] constructorArguments)
   at Castle.DynamicProxy.ProxyGenerator.CreateClassProxy(Type classToProxy, Type[] additionalInterfacesToProxy, ProxyGenerationOptions options, Object[] constructorArguments, IInterceptor[] interceptors)
   at Moq.CastleProxyFactory.CreateProxy(Type mockType, IInterceptor interceptor, Type[] interfaces, Object[] arguments) in /_/src/Moq/Interception/CastleProxyFactory.cs:line 110
   at Moq.Mock`1.InitializeInstance() in /_/src/Moq/Mock`1.cs:line 502
   at Moq.Mock`1.OnGetObject() in /_/src/Moq/Mock`1.cs:line 516
   at Moq.Mock.get_Object() in /_/src/Moq/Mock.cs:line 180
   at Moq.Mock`1.get_Object() in /_/src/Moq/Mock`1.cs:line 453
   at TcJiraServiceTest..ctor() in C:\Users\vuso1\RiderProjects\New\Neuer Ordner\trumpf.mailserveraccess\src\Trumpf.MailServerAccess.Tests\TcJiraServiceTest.cs:line 22
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean wrapExceptions)

System.MissingMethodException
Constructor on type 'Castle.Proxies.JiraProxy' not found.
   at System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture)
   at Castle.DynamicProxy.ProxyGenerator.CreateClassProxyInstance(Type proxyType, List`1 proxyArguments, Type classToProxy, Object[] constructorArguments)

解决方案

1. 解决Jira类无法Mock的核心问题

Moq动态代理需要类有可访问的无参构造函数,而Atlassian.Jira的Jira类没有提供。有两种可行方案:

方案A:给Mock传入构造函数参数

如果Jira类有公开的带参构造函数,创建Mock时传入匹配的假参数,让Moq能实例化代理类:

// 示例:匹配Jira类的Uri+Credential构造函数
var mockJiraClient = new Mock<Jira>(new Uri("https://fake-jira-instance.com"), new NetworkCredential("fake-user", "fake-pass"));

注意:参数必须严格匹配Jira类已有的构造函数签名,可以查看Atlassian.Jira的源码或文档确认。

方案B:抽象依赖(推荐长期方案)

自己定义接口封装TcJiraService需要的Jira客户端能力,通过适配器类对接真实的Jira客户端,让业务类依赖接口而非具体实现:

// 1. 定义接口,只暴露业务需要的方法
public interface IJiraClient
{
    Task<IEnumerable<Issue>> GetIssuesFromJqlAsync(string jql);
    Issue CreateIssue(string projectKey);
    Task<Issue> GetIssueAsync(string issueKey);
}

// 2. 实现适配器,对接真实Jira客户端
public class JiraClientAdapter : IJiraClient
{
    private readonly Jira _innerJira;

    public JiraClientAdapter(Jira innerJira)
    {
        _innerJira = innerJira;
    }

    public Task<IEnumerable<Issue>> GetIssuesFromJqlAsync(string jql)
    {
        return _innerJira.Issues.GetIssuesFromJqlAsync(jql);
    }

    public Issue CreateIssue(string projectKey)
    {
        return _innerJira.CreateIssue(projectKey);
    }

    public Task<Issue> GetIssueAsync(string issueKey)
    {
        return _innerJira.Issues.GetIssueAsync(issueKey);
    }
}

// 3. 修改TcJiraService的依赖为IJiraClient
public class TcJiraService(IJiraClient tJiraClient, IOptions<TcJiraOptions> tOptions, ILogger<TcJiraService> tLogger)
{
    // 原有业务逻辑不变,仅依赖类型替换
}

测试时直接Mock接口,完全避开Jira类的构造函数问题:

private readonly Mock<IJiraClient> mockJiraClient = new Mock<IJiraClient>();

2. 修正Issue的Mock问题

原测试中MockIssue也会遇到相同的构造函数限制,建议直接创建假对象而非Mock:

// 直接实例化Issue(如果允许)
var fakeIssue = new Issue(mockJiraClient.Object, "TEST", "1")
{
    Key = "TEST-1"
};
// 若属性是只读,可通过反射设置

3. 补全测试的Setup逻辑

原测试没有对CreateIssue和SaveChangesAsync做Mock,导致调用时会走真实逻辑,必须补充:

[Fact]
public async Task CreateJiraIssueAsync_ReturnsIssueKey_WhenCalledWithValidParameters()
{
    // Arrange
    var fakeIssue = new Mock<Issue>(mockJiraClient.Object, "TEST", "1"); 
    fakeIssue.Setup(issue => issue.Key).Returns("TEST-1");
    // Mock CreateIssue方法返回假Issue
    mockJiraClient.Setup(client => client.CreateIssue(It.IsAny<string>()))
                  .Returns(fakeIssue.Object);
    // Mock SaveChangesAsync为成功
    fakeIssue.Setup(issue => issue.SaveChangesAsync(default))
             .Returns(Task.CompletedTask);

    // Act
    var issueKey = await service.CreateJiraIssueAsync("Test Summary", "Test Description", "test@example.com");

    // Assert
    Assert.Equal("TEST-1", issueKey);
}

内容的提问来源于stack exchange,提问作者Haidepzai

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 15:54:52