Article summary
In my last post, Setting Up an Azure Functions Dependency Injection Context for Request Tests, I started working on request testing an Azure Functions project. The intent is to run through as much production code as possible.
Last time, I got the dependency injection context configured with the functions, configuration, and logging. In this post, I’m going to work on testing the Azure Functions’ handling of JSON input and output.
JSON Input
Azure Functions, at least in version 3.x, will automatically parse a JSON request body and pass the result in as an argument to the registered method.
Take the following Azure Function definition:
[FunctionName("CreateEntry")]
public async Task CreateEntryAsync(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "entries")]
HttpRequest req,
CreateEntryInput input)
{
...
}
The framework will automatically parse the incoming request body into a CreateEntryInput
instance. (My project is using the Newtonsoft Json.NET library.)
public class CreateEntryInput
{
public string Description { get; set; }
public string PartNumber { get; set; }
}
If I just instantiate an instance of CreateEntryInput
and pass it to the CreateEntryAsync
method in my test, I won’t be asserting that it will correctly parse the expected JSON input. For example, the documentation for the API says it accepts camelcase property names:
{
"description": "5/16 inch bolt",
"partNumber": "1234"
}
Without testing it, how can I be sure that the incoming JSON will be converted into the expected object correctly?
In order to get the same configuration that’s being used in the “real” Azure Function, we can get the JSON options from the dependency injection context:
var jsonBody = "{ \"description\": \"5/16 inch bolt\", \"partNumber\": \"1234\" }";
var jsonOptions =
host.Services.GetService<IOptions<MvcNewtonsoftJsonOptions>>();
var input = JsonConvert.DeserializeObject(
bodyText,
typeof(CreateEntryInput),
jsonOptions.Value.SerializerSettings);
var function = host.Services.GetRequiredService<MyAzureFunction>();
var request = new DefaultHttpContext().Request;
request.HttpContext.RequestServices = host.Services;
var actionResult =
await function.CreateEntryAsync(request, input);
(See the earlier post for setting up the DI context.)
JSON Output
Just as I want to verify that my Azure Functions are properly parsing incoming JSON, I want my tests to verify that the response body contains the expected JSON.
Similar to the input, Azure Functions will automatically serialize the IActionResult
returned from the bound method to JSON. This means that if you’re calling the bound method directly (as we are in a request test), the result will be an instance of IActionResult
, not a JSON string.
To turn the IActionResult
into JSON, we can instantiate an Microsoft.AspNetCore.Mvc.ActionContext
and execute it, just as the real Azure Functions host framework will do in production.
Continuing from the code snippet above:
request.HttpContext.Response.Body = new MemoryStream();
if (!(actionResult is NoContentResult))
{
var context = new ActionContext(
request.HttpContext,
request.HttpContext.GetRouteData(),
new ActionDescriptor());
await actionResult.ExecuteResultAsync(context);
// Rewind the stream so it can be read on the next line
request.HttpContext.Response.Body.Position = 0;
var responseBodyJson =
await new StreamReader(request.HttpContext.Response).ReadToEndAsync();
... // Assert responseBodyJson matches expectation
}
After executing the result, the response body stream can be read to get the resulting JSON text, and then assertions can be made.
Conclusion
I’m now able to test that if some expected JSON is passed into my method, I’ll get back the expected JSON, moving us closer to an end-to-end request test for an Azure Function, while still running in a normal “unit” test environment.
Good article,
The input and output binders are part of the framework. I don’t see value on ‘unit testing’ third party dependencies… that’s not our responsibility and incurs on additional effort with no real value.
For unit testing I would only focus on our implementation in complete isolation from any external system.
If we wanted to test the system as a whole (we should also consider triggers, DI system, Environment variables, …) I would better just create a set of API tests consuming the Functions as a black box.
Robert,
Thanks for the feedback. I agree that you’d get more coverage if you wrote external tests and treated the Functions as a black box. But what I’ve been trying to determine with these experiments and blog posts, is whether it’s possible to get “pretty close” to the end-to-end, while still staying in the friendly confines of the .NET unit test framework (test-runner built into the IDE, same debugging capabilities, everything is in C#.
It’s all about trade offs. If there’s a way to test very close to the edges of the Functions, and do it in a way that’s going to keep the development team efficient and comfortable, that could result in a better test suite than one that’s written in a scripting language and treats the API like a black box.
And just to clarify, even though I’m using a “unit testing” framework and tools, I do not consider these unit tests. I’ve been using the term “request” tests, but I think integration or even system tests would be a more accurate name.