2

The documentation for Durable Function Testing only talks about the in-proc model - https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-unit-testing

I have a timer-triggered orchestrator as below -

 public class Orchestrator
 {
     private IMapper mapper;
     private IRepository repository;

     public Orchestrator(IMapper mapper, IRepository repository)
     {
         this.mapper = mapper;
         this.repository = repository;
     }

     [Function(nameof(Orchestrator))]
     public async Task RunOrchestrator(
         [OrchestrationTrigger] TaskOrchestrationContext context)
     {
         ILogger logger = context.CreateReplaySafeLogger(nameof(ConnectorOrchestrator));

         IEnumerable<Result> results;

         try
         {
             results = await repository.GetAllResultsAsync();
         }
         catch (Exception ex)
         {
             logger.LogError(ex, $"Error getting results.");
             throw;
         }

         foreach (var result in results)
         {
             try
             {
                 _ = context.CallActivityAsync<string>(nameof(Activity), result);
             }
             catch (Exception ex)
             {
                 logger.LogError(ex, $"Error calling activity.");
                 throw;
             }
         }
     }

     [Function(nameof(Activity))]
     public void ProcessAlerts([ActivityTrigger] Result result, FunctionContext executionContext)
     {

         logger.LogInformation($"Activity started.");

         logger.LogInformation($"Activity completed");
     }

     [Function("Orchestrator_ScheduledStart")]
     public async Task ScheduledStart(
         [TimerTrigger("* */15 * * * *")] TimerInfo timerInfo,
         [DurableClient] DurableTaskClient client,
         FunctionContext executionContext)
     {
         ILogger logger = executionContext.GetLogger("Orchestrator_ScheduledStart");

         string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
             nameof(ConnectorOrchestrator));

         logger.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId);
     }
 }

In the below test, I get an error that DurableTaskClient cannot be mocked -

public OrchestratorTests()
{
    mapper = new Mock<IMapper>();
    repository = new Mock<IRepository>();
    durableClient = new Mock<DurableTaskClient>();
    connectorOrchestrator = new ConnectorOrchestrator(mapper.Object, repository.Object);
}

[Fact]
public async Task ScheduledStart_ShouldTriggerOrchestrator()
{
    TimerInfo timerInfo = new TimerInfo();
    Mock<FunctionContext> functionContext = new Mock<FunctionContext>();
    await connectorOrchestrator.ScheduledStart(timerInfo, durableClient.Object, functionContext.Object);

    durableClient.Verify(client => client.ScheduleNewOrchestrationInstanceAsync(nameof(Orchestrator), null, null, default), Times.Once);
}

Is there any way to test isolated durable orchestrators today?

1 Answer 1

5

This can be achieved with a bit of boilerplate code and some fakes. What I personally did is :

  • Create a FakeDurableTaskClient class implementing DurableTaskClient.
  • Instantiate a Mock and pass its Object field (here of type FakeDurableTaskClient) to the method I want to test. My fake class looks as follows :
public class FakeDurableTaskClient : DurableTaskClient
{
    public FakeDurableTaskClient() : base("fake")
    {
    }

    public override Task<string> ScheduleNewOrchestrationInstanceAsync(TaskName orchestratorName, object input = null, StartOrchestrationOptions options = null,
        CancellationToken cancellation = new())
    {
        return Task.FromResult(options?.InstanceId ?? Guid.NewGuid().ToString());
    }

    public override Task RaiseEventAsync(string instanceId, string eventName, object eventPayload = null, CancellationToken cancellation = new())
    {
        return Task.CompletedTask;
    }

    public override Task<OrchestrationMetadata> WaitForInstanceStartAsync(string instanceId, bool getInputsAndOutputs = false,
        CancellationToken cancellation = new())
    {
        return Task.FromResult(new OrchestrationMetadata(Guid.NewGuid().ToString(), instanceId));
    }

    public override Task<OrchestrationMetadata> WaitForInstanceCompletionAsync(string instanceId, bool getInputsAndOutputs = false,
        CancellationToken cancellation = new())
    {
        return Task.FromResult(new OrchestrationMetadata(Guid.NewGuid().ToString(), instanceId));
    }

    public override Task TerminateInstanceAsync(string instanceId, object output = null, CancellationToken cancellation = new())
    {
        return Task.CompletedTask;
    }

    public override Task SuspendInstanceAsync(string instanceId, string reason = null, CancellationToken cancellation = new())
    {
        return Task.CompletedTask;
    }

    public override Task ResumeInstanceAsync(string instanceId, string reason = null, CancellationToken cancellation = new())
    {
        return Task.CompletedTask;
    }

    public override Task<OrchestrationMetadata> GetInstancesAsync(string instanceId, bool getInputsAndOutputs = false,
        CancellationToken cancellation = new())
    {
        return Task.FromResult(new OrchestrationMetadata(Guid.NewGuid().ToString(), instanceId));
    }

    public override AsyncPageable<OrchestrationMetadata> GetAllInstancesAsync(OrchestrationQuery filter = null)
    {
        return new FakeOrchestrationMetadataAsyncPageable();
    }

    public override Task<PurgeResult> PurgeInstanceAsync(string instanceId, CancellationToken cancellation = new())
    {
        return Task.FromResult(new PurgeResult(1));
    }

    public override Task<PurgeResult> PurgeAllInstancesAsync(PurgeInstancesFilter filter, CancellationToken cancellation = new())
    {
        return Task.FromResult(new PurgeResult(Random.Shared.Next()));
    }

    public override ValueTask DisposeAsync()
    {
        return ValueTask.CompletedTask;
    }
}

I also had to create a fake of AsyncPageable :

internal class FakeOrchestrationMetadataAsyncPageable : AsyncPageable<OrchestrationMetadata>
{
    public override IAsyncEnumerable<Page<OrchestrationMetadata>> AsPages(string continuationToken = null, int? pageSizeHint = null)
    {
        return AsyncEnumerable.Empty<Page<OrchestrationMetadata>>();
    }
}

If I take your example, the following should work using those fakes :

public OrchestratorTests()
{
    mapper = new Mock<IMapper>();
    repository = new Mock<IRepository>();
    durableClient = new Mock<FakeDurableTaskClient>();
    connectorOrchestrator = new ConnectorOrchestrator(mapper.Object, repository.Object);
}

[Fact]
public async Task ScheduledStart_ShouldTriggerOrchestrator()
{
    TimerInfo timerInfo = new TimerInfo();
    Mock<FunctionContext> functionContext = new Mock<FunctionContext>();
    await connectorOrchestrator.ScheduledStart(timerInfo, durableClient.Object, functionContext.Object);

    durableClient.Verify(client => client.ScheduleNewOrchestrationInstanceAsync(nameof(Orchestrator), null, null, default), Times.Once);
}

I cannot guarantee it works with every use-case (I didn't test it with the Entities so far) and it doesn't really feel natural. I would appreciate a more comfortable out-of-the-box solution. But until now, I don't have any better idea.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you, this was so helpful! In a similar way, I'm trying to mock the TaskOrchestrationContext. In this, I'm unable to ovverride or setup 'CreateReplaySafeLogger' because it's not abstract. Every time the code reaches this point, it throws a null reference exception. I can see that it is internally using ILoggerFactory, but does that mean I mean I need to mock ILoggerFactory too, and then override the LoggerFactory method in FakeTaskOrchestrationContext?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.