单元测试一个定时触发的 Azure Function

9
我有一个定时触发的 Azure Function,想要使用 XUnit 和 MOQ 进行测试。
在我知道需要使用该类的实例(例如funTimeTriggeredObj)调用类的Run方法。
funTimeTriggered funTimeTriggeredObj = new funTimeTriggered(queueSchedulerMock.Object, telemetryHelperMock.Object)

funTimeTriggeredObj.Run(param1, param2, loggerMock.Object) 

where

private Mock<ILogger> loggerMock = new Mock<ILogger>() 

我不确定如何模拟参数param1param2,它们分别是TimerInfoExecutionContext对象。

我之所以问这个问题是因为既TimerInfo,也ExecutionContext都没有实现可以被模拟的接口。

下面是我的实际函数实现。任何帮助都将不胜感激。

using System;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;

public  class funTimeTriggered
{
    private  string  _invocationID;
    private readonly IQueueScheduler _queueScheduler;
    private readonly ITelemetryHelper _telemetryHelper;

    public funTimeTriggered(IQueueScheduler queueScheduler, ITelemetryHelper telemetryHelper)
    {
        _queueScheduler = queueScheduler;
        _telemetryHelper = telemetryHelper;
    }

    [FunctionName("funTimeTriggered")]
    public  async Task Run([TimerTrigger("0/10 * * * * *")]TimerInfo myTimer, ExecutionContext context, ILogger log)
    {
        log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
        try
        {
            _invocationID = context.InvocationId.ToString();
            await _queueScheduler.SendEventsToServiceBusAndDeleteFromSQS();
        }
        catch (Exception ex)
        {
            log.LogError(ex.Message);
            _telemetryHelper.LogException(ex);
            throw ex;
        }
    }
}

这里有一些关于当你没有类的所有权时该如何模拟(因此无法像在这种情况下需要的那样标记方法/属性为虚拟)的讨论:https://dev59.com/h2Ij5IYBdhLWcg3wTTgG - Kristin
1
你真的需要模拟它们吗?它们似乎在你的代码中根本没有被使用。我还认为它们都有公共构造函数而且没有副作用,所以你甚至可以传递一个实际的实例进去。 - NotFound
Azure Function的定时触发器由Azure Function SDK处理,您无法对其编写单元测试。您应该为自己的代码编写单元测试。 - Pankaj Rawat
1
@PankajRawat 我已经为业务逻辑编写了一个单独的单元测试,并将其保存在不同的类中;这个要求特别是为了编写一个“功能测试”。 - The Inquisitive Coder
你将会在创建一个新的 TimerInfo 实例时遇到困难,因为它需要一个抽象类 TimerSchedule 的实例。但是我通过伪造来避免了这个问题。你也应该这样做,因为这不是你正在测试的内容。你可以为你的方法可能从 TimerInfo 调用的某些方法伪造返回值。 - Met-u
5个回答

11

如果使用这些类的实际实例没有不良影响,而且您可以实际初始化它们,那么请创建实际实例并将它们传递给要测试的函数。

如果使用实际实例没有不良影响,则不必使用接口或模拟。

//Arrange

//...omitted for brevity

var param1 = new TimerInfo(...); 
var param2 = = new ExecutionContext {
    InvocationId = Guid.NewGuid()
};

//Act
await funTimeTriggeredObj.Run(param1, param2, loggerMock.Object);

//Assert
//...assert expected behavior

而且,由于在这个测试案例中计时器并未被函数使用,因此可以完全忽略它。

//Arrange

//...omitted for brevity

var param1 = default(TimerInfo); //null
var param2 = = new ExecutionContext {
    InvocationId = Guid.NewGuid()
};

//Act
await funTimeTriggeredObj.Run(param1, param2, loggerMock.Object);

//Assert
//...assert expected behavior

4
设置是正确的。但是,你可以直接发送 null,而不是试图模拟 TimerInfo 和 ExecutionContext,或者实现它们,因为你的函数内部并没有使用它们。

3

你可以将Azure函数的逻辑放入一个单独的类中,并为该类编写单元测试。

如果创建了另一个使用不同触发器(例如HTTP)进行相同操作的函数,则可以进行集成测试。


1
我为业务逻辑编写了一个单独的单元测试,并将其保存在不同的类中;这个要求是专门为了编写“功能测试”。 - The Inquisitive Coder

1
 // Arrange
 TimerSchedule schedule = new DailySchedule("2:00:00");
 TimerInfo timerInfo = new TimerInfo(schedule, It.IsAny<ScheduleStatus>(), false);
    
 // Act
 await _functions.TimerTrigerFunction(timerInfo, _durableOrchestrationClient.Object, _log.Object);

0

我之前也有类似的测试需要完成。不过我使用了FakeItEasy,但我希望这个方法对你(或其他人)仍然有所帮助。

所使用的软件包:

<PackageReference Include="FakeItEasy" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />

一些测试:

public class FunTimeTriggeredConstructorTests
{
    private ITelemetryHelper _fakeITelemetryHelper;
    private IQueueScheduler _fakeIQueueScheduler;

    public FunTimeTriggeredConstructorTests()
    {
        _fakeITelemetryHelper= A.Fake<ITelemetryHelper >();
        _fakeIQueueScheduler = A.Fake<IQueueScheduler>();
    }

    [Fact]
    public void ShouldThrow_ArgumentNullException_When_IQueueScheduler_Is_Null()
    {
        _fakeIQueueScheduler = null;

        Assert.Throws<ArgumentNullException>(() => new FunTimeTriggered(_fakeITelemetryHelper, _fakeIQueueScheduler));
    }
}

public class RunTests
{
    private ITelemetryHelper _fakeITelemetryHelper;
    private IQueueScheduler _fakeIQueueScheduler;
    private TimerInfo _fakeTimerInfo;
    private ExecutionContext _fakeExecutionContext;
    private ILogger _fakeILogger;
    private FunTimeTriggered _funTimeTriggered;

    public RunTests()
    {
        _fakeITelemetryHelper= A.Fake<ITelemetryHelper>();
        _fakeTimerInfo = A.Fake<TimerInfo>();
        _fakeIQueueScheduler = A.Fake<IQueueScheduler>();
        _fakeExecutionContext = A.Fake<ExecutionContext>();
        _fakeILogger = A.Fake<ILogger>();

        _funTimeTriggered = new FunTimeTriggered(_fakeITelemetryHelper, _fakeIQueueScheduler);
    }

    [Fact]
    public async Task ShouldCall_IQueueScheduler_SendEventsToServiceBusAndDeleteFromSQS_AtMost_Once()
    {
        A.CallTo(() => _fakeExecutionContext.InvocationId).Returns("");

        await FunTimeTriggered.Run(_fakeTimerInfo, _fakeExecutionContext, _fakeILogger);

        A.CallTo(() => _queueScheduler.SendEventsToServiceBusAndDeleteFromSQS()).MustHaveHappenedOnceExactly();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接