Unit Test a Time Triggered Azure Function
Asked Answered
W

5

10

I've got a time-triggered Azure Function which I want to test with XUnit and MOQ.

While I know I need to call the Run method of the class using an instance of the class say funTimeTriggeredObj where

funTimeTriggered funTimeTriggeredObj = new funTimeTriggered(queueSchedulerMock.Object, telemetryHelperMock.Object)

like

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

where

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

I'm not sure how should I mock the param1 & param2 which are TimerInfo and ExecutionContext objects respectively.

The reason why I'm asking is because neither 'TimerInfo' nor 'ExecutionContext' implements any interface which can be mocked.

Below is my actual function implementation. Any help whatsoever would be highly appreciated.

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;
        }
    }
}
Wharfinger answered 1/4, 2020 at 8:59 Comment(5)
There are some discussions of what to do when you have no ownership of the class you're trying to mock (and thus can't mark methods/properties as virtual as needed in this case) here: #20401234Lectern
Do you really need to mock them? They don't seem used at all in your of code. I also think they both have public constructors with no side-effects so you could even pass an actual instance in.Spradling
Azure Function Timer trigger handle by Azure Function SDK, you can't write unit test on that. you should write a unit test for your own codeLiquate
@PankajRawat I've written a separate unit test for the business logic which I've kept in a different class; this requirement was specifically to write a 'function test'Wharfinger
You'll have a tough time creating a new instance of TimerInfo because it requires an instance of TimerSchedule which is an abstract class. But I just avoided the drama by faking it. You should also just fake it anyway because it's not really what you testing. You could always just fake returns for certain methods that your method might call from TimerInfo.Goffer
C
12

If there are no undesired effects of using actual instance of those classes and you can actually initialize them then create actual instance and pass them to the function under test.

They do not have to be interfaces or mocked if using the actual instance(s) has no unwanted effects

//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

And since in this test case the timer is not even used by the function, it can be ignored altogether

//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
Choline answered 1/4, 2020 at 12:9 Comment(0)
E
4

You could put the logic of your Azure function into a separate class and write unit tests for that class.

What could be done is an integration test, if there was a another function created with a different trigger (for example HTTP), doing the same thing.

Earn answered 1/4, 2020 at 11:58 Comment(1)
I've written a separate unit test for the business logic which I've kept in a different class; this requirement was specifically to write a 'function test'Wharfinger
D
4

The setup is right. But instead of trying to mock TimerInfo and ExecutionContext, or implementing them, you can simply send null since you don't use them inside your function.

Douro answered 21/8, 2020 at 0:3 Comment(0)
T
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);
Tolerate answered 21/7, 2021 at 8:34 Comment(0)
G
0

I had similar tests that I needed to do. I used FakeItEasy though, but I'm hoping this will help you (or others) nonetheless.

Packages used:

<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" />

Some tests:

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();
    }
}
Goffer answered 28/8, 2020 at 14:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.