Reputation: 1470
I've started developing Azure Functions and now I want to create my first unit/integration test, but I'm completely stuck. Although I have a very simple Function with an HTTP Trigger and HTTP and Storage Queue output, it seems ridiculously complex te test this.
The code (simplified):
public class MyOutput
{
[QueueOutput("my-queue-name", Connection = "my-connection")]
public string QueueMessage { get; set; }
public HttpResponseData HttpResponse { get; set; }
}
public static class MyFunction
{
[Function(nameof(MyFunction))]
public static async Task<MyOutput> Run(
[HttpTrigger(AuthorizationLevel.Function, "POST")] HttpRequestData req,
FunctionContext executionContext)
{
var logger = executionContext.GetLogger(nameof(MyFunction));
logger.LogInformation("Received {Bytes} bytes", req.Body.Length);
//implementation
}
}
Now I'd expect to build a test like this:
public async Task Test()
{
var response = await MyFunction.Run(..., ...);
Assert.IsNotNull(response);
}
After looking hours on the internet to find a good approach, I still didn't find a way to mock HttpRequestData
and FunctionContext
. I also looked for a full integration test by setting up a server, but this seems really complex. The only thing I ended up was this: https://github.com/Azure/azure-functions-dotnet-worker/blob/72b9d17a485eda1e6e3626a9472948be1152ab7d/test/E2ETests/E2ETests/HttpEndToEndTests.cs
Does anyone have experience testing Azure Functions in .NET 5, who can give me a push in the right direction? Are there any good articles or examples on how to test an Azure Function in dotnet-isolated?
Upvotes: 38
Views: 20129
Reputation: 649
For what it's worth, this is my implementation using .NET 7 and Azure Functions v4. It expands on the work of @VincentBitter and is a workable, multi-request capable HttpClientFactory mock.
The main thing to note here is the addition of the
serviceCollection.AddFunctionsWorkerDefaults();
MockHttpClientFactory
public class MockHttpClientFactory
{
public static IHttpClientFactory Create(string name, MockHttpResponse response)
{
return Create(name, new List<MockHttpResponse> { response });
}
public static IHttpClientFactory Create(string name, List<MockHttpResponse> responses)
{
Mock<HttpMessageHandler> messageHandler = SendAsyncHandler(responses);
var mockHttpClientFactory = new Mock<IHttpClientFactory>();
mockHttpClientFactory
.Setup(x => x.CreateClient(name))
.Returns(new HttpClient(messageHandler.Object)
{
BaseAddress = new Uri("https://mockdomain.mock")
});
return mockHttpClientFactory.Object;
}
private static Mock<HttpMessageHandler> SendAsyncHandler(List<MockHttpResponse> responses)
{
var messageHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
foreach(var response in responses)
{
messageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.Is<HttpRequestMessage>(r => r.RequestUri!.PathAndQuery == response.UrlPart),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = response.StatusCode,
Content = (response.Response?.GetType() == typeof(string)
? new StringContent(response.Response?.ToString() ?? "")
: new StringContent(JsonSerializer.Serialize(response.Response)))
})
.Verifiable();
}
return messageHandler;
}
}
MockHttpResponse
public class MockHttpResponse
{
public MockHttpResponse()
{
}
public MockHttpResponse(string urlPart, object response, HttpStatusCode statusCode)
{
this.UrlPart = urlPart;
this.Response = response;
this.StatusCode = statusCode;
}
public string UrlPart { get; set; } = String.Empty;
public object Response { get; set; } = default!;
public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;
}
MockHttpRequestData
public class MockHttpRequestData
{
public static HttpRequestData Create()
{
return Create<string>("");
}
public static HttpRequestData Create<T>(T requestData) where T : class
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddFunctionsWorkerDefaults();
var serializedData = JsonSerializer.Serialize(requestData);
var bodyDataStream = new MemoryStream(Encoding.UTF8.GetBytes(serializedData));
var context = new Mock<FunctionContext>();
context.SetupProperty(context => context.InstanceServices, serviceCollection.BuildServiceProvider());
var request = new Mock<HttpRequestData>(context.Object);
request.Setup(r => r.Body).Returns(bodyDataStream);
request.Setup(r => r.CreateResponse()).Returns(new MockHttpResponseData(context.Object));
return request.Object;
}
}
MockHttpResponseData
public class MockHttpResponseData : HttpResponseData
{
public MockHttpResponseData(FunctionContext functionContext) : base(functionContext)
{
}
public override HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;
public override HttpHeadersCollection Headers { get; set; } = new HttpHeadersCollection();
public override Stream Body { get; set; } = new MemoryStream();
public override HttpCookies Cookies { get; }
}
The Azure Function Method
This azure function has been setup with DI and uses an HttpClient object. Details are out of scope for this post. You can Google for more info.
public class Function1
{
private readonly HttpClient httpClient;
public Function1(IHttpClientFactory httpClientFactory)
{
this.httpClient = httpClientFactory.CreateClient("WhateverYouNamedIt");
}
[Function("Function1")]
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
{
var httpResponse = await this.httpClient.GetAsync("/some-path");
var httpResponseContent = await httpResponse.Content.ReadAsStringAsync();
// do something with the httpResponse or Content
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteStringAsync(httpResponseContent);
return response;
}
}
Simple Use Case
public class UnitTest1
{
[Fact]
public void Test1()
{
var httpClientFactory = MockHttpClientFactory.Create("WhateverYouNamedIt", new MockHttpResponse());
var exception = Record.Exception(() => new Function1(httpClientFactory));
Assert.Null(exception);
}
}
More Realistic Use Case
[Fact]
public async Task Test2()
{
var httpResponses = new List<MockHttpResponse>
{
new MockHttpResponse
{
UrlPart = "/some-path",
Response = new { Name = "data" }
}
};
var httpClientFactory = MockHttpClientFactory.Create("WhateverYouNamedIt", httpResponses);
var httpRequestData = MockHttpRequestData.Create();
var function1 = new Function1(httpClientFactory);
var function1Response = await function1.Run(httpRequestData);
function1Response.Body.Position = 0;
using var streamReader = new StreamReader(function1Response.Body);
var function1ResponseBody = await streamReader.ReadToEndAsync();
Assert.Equal("{\"Name\":\"data\"}", function1ResponseBody);
}
Upvotes: 4
Reputation: 1149
You can mock the context and provide only what you will use from it. Here you have an approach using NSubstitute to obtain an ILogger you can customize as you need:
[TestClass]
public class FunctionTest
{
private static readonly FunctionContext _context = Substitute.For<FunctionContext>();
private static readonly ILogger _logger = Substitute.For<ILogger>();
[ClassInitialize]
public static void ClassSetupAsync(TestContext _)
{
// create a mock log factory that returns a mocked logger
var logFactory = Substitute.For<ILoggerFactory>();
logFactory
.CreateLogger(Arg.Any<string>())
.Returns(_logger);
// create a mock service provider that knows only about logs
var services = Substitute.For<IServiceProvider>();
services
.GetService(Arg.Any<Type>())
.Returns(logFactory);
// use the mocked service provider in the mocked context
// you can pass this context to your Azure Function
_context
.InstanceServices
.Returns(services);
}
}
Upvotes: 1
Reputation: 1470
I was finally able to mock the whole thing. Definitely not my best work and can use some refactoring, but at least I got a working prototype:
var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<ILoggerFactory, LoggerFactory>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var context = new Mock<FunctionContext>();
context.SetupProperty(c => c.InstanceServices, serviceProvider);
var byteArray = Encoding.ASCII.GetBytes("test");
var bodyStream = new MemoryStream(byteArray);
var request = new Mock<HttpRequestData>(context.Object);
request.Setup(r => r.Body).Returns(bodyStream);
request.Setup(r => r.CreateResponse()).Returns(() =>
{
var response = new Mock<HttpResponseData>(context.Object);
response.SetupProperty(r => r.Headers, new HttpHeadersCollection());
response.SetupProperty(r => r.StatusCode);
response.SetupProperty(r => r.Body, new MemoryStream());
return response.Object;
});
var result = await MyFunction.Run(request.Object, context.Object);
result.HttpResponse.Body.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(result.HttpResponse.Body);
var responseBody = await reader.ReadToEndAsync();
Assert.IsNotNull(result);
Assert.AreEqual(HttpStatusCode.OK, result.HttpResponse.StatusCode);
Assert.AreEqual("Hello test", responseBody);
I added the Logger via Dependency Injection and created my own implementations for HttpRequestData and HttpResponseData. This is way easier to re-use and makes the tests itself cleaner.
public class FakeHttpRequestData : HttpRequestData
{
public FakeHttpRequestData(FunctionContext functionContext, Uri url, Stream body = null) : base(functionContext)
{
Url = url;
Body = body ?? new MemoryStream();
}
public override Stream Body { get; } = new MemoryStream();
public override HttpHeadersCollection Headers { get; } = new HttpHeadersCollection();
public override IReadOnlyCollection<IHttpCookie> Cookies { get; }
public override Uri Url { get; }
public override IEnumerable<ClaimsIdentity> Identities { get; }
public override string Method { get; }
public override HttpResponseData CreateResponse()
{
return new FakeHttpResponseData(FunctionContext);
}
}
public class FakeHttpResponseData : HttpResponseData
{
public FakeHttpResponseData(FunctionContext functionContext) : base(functionContext)
{
}
public override HttpStatusCode StatusCode { get; set; }
public override HttpHeadersCollection Headers { get; set; } = new HttpHeadersCollection();
public override Stream Body { get; set; } = new MemoryStream();
public override HttpCookies Cookies { get; }
}
Now the test looks like this:
// Arrange
var body = new MemoryStream(Encoding.ASCII.GetBytes("{ \"test\": true }"))
var context = new Mock<FunctionContext>();
var request = new FakeHttpRequestData(
context.Object,
new Uri("https://stackoverflow.com"),
body);
// Act
var function = new MyFunction(new NullLogger<MyFunction>());
var result = await function.Run(request);
result.HttpResponse.Body.Position = 0;
// Assert
var reader = new StreamReader(result.HttpResponse.Body);
var responseBody = await reader.ReadToEndAsync();
Assert.IsNotNull(result);
Assert.AreEqual(HttpStatusCode.OK, result.HttpResponse.StatusCode);
Assert.AreEqual("Hello test", responseBody);
Upvotes: 60
Reputation: 1565
To complement what you have been doing, I was trying to mock GetLogger()
so that I could inject ILogger; unlucky enough, GetLogger()
is an extension (static) so it cannot be mocked through reflection. I am now on the way to mocking the fields which are used by the GetLogger()
extension (.Net source code).
That would look like this:
using Mock;
using Microsoft.Extensions.DependencyInjection;
public static FunctionContext CreateFunctionContext(ILogger logger = null)
{
logger = logger ?? CreateNullLogger();
var LoggerFactory = new Mock<ILoggerFactory>();
LoggerFactory.Setup(p => p.CreateLogger(It.IsAny<string>())).Returns(logger);
var InstanceServices = new Mock<IServiceProvider>();
InstanceServices.Setup(p => p.GetService(It.IsAny<Type>())).Returns(LoggerFactory.Object);
var context = new Mock<FunctionContext>();
context.Setup(p => p.InstanceServices).Returns(InstanceServices.Object);
return context.Object;
}
Upvotes: 4
Reputation: 910
Calling the Run method of your Function is more a unit than an integration test. Think about some middleware you have set up or authentication that will not run when calling the Function directly.
However, it seems, currently, there is no elegant, in-process way to do integration testing with Azure Isolated Functions based on .NET 5. Refer to: https://github.com/Azure/azure-functions-dotnet-worker/issues/541 and https://github.com/Azure/azure-functions-dotnet-worker/issues/281
I had some success by starting the Azure Function Host "func" locally, hosting my functions and then using a normal HttpClient from the Test itself. Before that, you can set up your other dependencies which you don't want to mock, such as Azurite and some blob data.
Upvotes: 0