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
相关产品推荐
相关产品推荐

