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

Azure Function Xunit Mock测试报错:期望调用1次实际为0次

问题现象

运行针对TimerTrigger类型Azure Function DeleteJob的Xunit单元测试时,Moq校验失败,提示GetBlobBeforeDate方法预期被调用1次,实际调用次数为0。


根因排查

代码存在4处明显错误,直接导致逻辑未执行到依赖调用环节、校验不通过:

  • CancellationToken变量名拼写错误,触发异常被吞:DeleteBlobMetadata方法的入参名定义为canceltoken(拼写错误,正确命名应为cancellationToken,对应类型CancellationToken本身为双l拼写:Cancellation),但方法内第一行判断取消状态时写的是if (cancellationToken.IsCancellationRequested),该行直接触发变量未定义的运行时异常,被方法内的catch(Exception)块捕获后仅打印日志就退出,完全没走到后续调用_store.GetBlobBeforeDate的逻辑,这是调用次数为0的核心原因。
  • 删除方法名不匹配:业务代码中调用的Blob删除方法是_store.DeleteBlobAsync,但单元测试的Setup和Verify逻辑全针对DeleteAttachmentAsync,方法名完全不一致,就算走到删除逻辑也会校验失败。
  • 类字段缺失:DeleteJob类中仅声明了_store字段,未声明_log字段,但构造函数中直接给_log赋值,会触发编译/运行错误。
  • 测试代码缺失SUT构造逻辑:测试代码中调用了GetSut()获取被测实例,但给出的代码中未实现该方法,若该方法未正确传入Mock的_StoreMock.Object和_logMock.Object,也会导致Mock依赖未被实际调用。

修正后代码

业务代码修正

public class DeleteJob
{
    private readonly IStore _store;
    // 补全缺失的_log字段声明
    private readonly ILogger<DeleteJob> _log;

    public DeleteJob(IStore store, ILogger<DeleteJob> log)
    {
        _store = store;
        _log = log;
    }

    [Function("DeleteJob")]
    public async Task Run([TimerTrigger("0 */5 * * * *", RunOnStartup = false)] MyInfo myTimer)
    {
        
        var canceltokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(8));
        var deleteDate = DateTime.UtcNow.AddMonths(-6);
        try
        {
            await DeleteBlobMetadata(deleteDate, canceltokenSource.Token);
        }
        catch (OperationCanceledException)
        {
            _log.LogInformation("Function ran out of time");
        }
       
    }

    private async Task DeleteBlobMetadata(DateTime deleteDate, CancellationToken cancellationToken)
    {
        try
        {
            // 修正token变量名拼写,和入参保持一致
            if (cancellationToken.IsCancellationRequested)
                return;
            var BlobUrls = await _store.GetBlobBeforeDate(Constants.ContainerName, deleteDate);
            foreach (var blobName in BlobUrls)
            {
                if (cancellationToken.IsCancellationRequested)
                    return;
                await _store.DeleteBlobAsync(Constants.ContainerName, blobName);
            }
        }
        catch (Exception e) 
        {
            _log.LogError($"Exception when deleting attachments: \n{e}");
            // 测试阶段建议抛出异常,方便定位问题
            throw;
        }
    }
}

单元测试代码修正

补全GetSut方法,修正校验的方法名:

public class DeleteTest
{
    private readonly Mock<IStore> _StoreMock;
    private readonly Mock<ILogger<DeleteJob>> _logMock;

    public DeleteTest()
    {
        _StoreMock = new Mock<IStore>();
        _logMock = new Mock<ILogger<DeleteJob>>();
    }

    [Fact]
    public async Task DeleteBlobOlderThan6Months_ShouldDelete()
    {
        SetupDeletionSuccessful(true);
        SetupDeleteBlobSetup();
        var sut = GetSut();
        await sut.Run(myTimer: null);
        _StoreMock.Verify(m => m.GetBlobBeforeDate(It.IsAny<string>(),It.IsAny<DateTime>()), Times.Exactly(1));
        // 修正校验的方法名为DeleteBlobAsync,和业务代码保持一致
        _StoreMock.Verify(m => m.DeleteBlobAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Exactly(1));
    }

    private void SetupDeleteBlobSetup()
    {
        _StoreMock.Setup(m => m.GetBlobBeforeDate(It.IsAny<string>(),It.IsAny<DateTime>()))
                .ReturnsAsync(new List<string> { "someUrl" });
    }

    private void SetupDeletionSuccessful(bool successfulDeletion)
    {
        // 修正Setup的方法名为DeleteBlobAsync
        _StoreMock.Setup(m => m.DeleteBlobAsync(It.IsAny<string>(), It.IsAny<string>())).ReturnsAsync(successfulDeletion);
    }

    // 补全SUT构造方法,传入Mock的依赖实例
    private DeleteJob GetSut()
    {
        return new DeleteJob(_StoreMock.Object, _logMock.Object);
    }
}

额外建议
  • 不要在业务代码里用无差别catch(Exception)吞所有异常,至少在开发/测试阶段保留异常抛出逻辑,方便快速定位问题
  • 开启IDE的拼写检查、将编译警告设置为错误,能提前发现变量名、方法名拼写错误这类低级问题
  • 如果Run方法后续会用到myTimer的属性(比如ScheduleStatus),建议Mock一个MyInfo实例传入,不要直接传null,避免空引用异常

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 18:31:00