ASP.NET Core – How to unit test a custom InputFormatter

In this article, I’ll show how to unit test a custom InputFormatter. The main thing to test is the output of the ReadRequestBodyAsync() method. To test this, you have to pass in an InputFormatterContext object containing the request body.

As an example, I’ll show how to unit test the following ReadRequestBodyAync() method:

public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
	using (var reader = new StreamReader(context.HttpContext.Request.Body))
	{
		string csv = await reader.ReadToEndAsync();
		var values = csv.Split(",");

		if (values[0] == "bad")
		{
			context.ModelState.TryAddModelError("Body", "Invalid value in CSV!");
			return InputFormatterResult.Failure();
		}

		return InputFormatterResult.Success(values);
	}
}
Code language: C# (cs)

Note: This parses CSV from a text/csv request.

1 – Build the InputFormatterContext

In order to test the ReadRequestBodyAsync() method, you need to first create the InputFormatterContext object and load the request body into it. I suggest putting this in a helper method, such as the following:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using System.Text;

static InputFormatterContext BuildContext(string content, string contentType, Type modelType)
{
	var httpContext = new DefaultHttpContext();
	httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(content));
	httpContext.Request.ContentType = contentType;
	httpContext.Request.ContentLength = httpContext.Request.Body.Length;

	return new InputFormatterContext(httpContext,
		string.Empty,
		new ModelStateDictionary(),
		new EmptyModelMetadataProvider().GetMetadataForType(modelType),
		(stream, encoding) => new HttpRequestStreamReader(stream, encoding),
		false);
}
Code language: C# (cs)

As you can see, InputFormatterContext has many constructor parameters. The most important part is setting HttpContext.Request.Body to a MemoryStream containing your content.

Note: Use this as a starting point and make adjustments if necessary depending on what your input formatter is doing.

2 – Add a unit test

Here’s an example of unit testing the happy path. Given the request body “a,b,c”, we are asserting that the InputFormatter returns a string[] object containing “a”, “b”, “c”:

[TestMethod()]
public async Task TestReadBody_WhenValidBodyContent_ReturnsParsedValues()
{
	//arrange
	var csvStringInputFormatter = new CsvStringInputFormatter();

	string content = "a,b,c";
	string contentType = "text/csv";
	Type modelType = typeof(string[]);

	var inputFormatterContext = BuildContext(content, contentType, modelType);

	//act
	var result = await csvStringInputFormatter.ReadRequestBodyAsync(inputFormatterContext);
	string[] model = result.Model as string[];

	//assert
	Assert.IsFalse(result.HasError);
	CollectionAssert.AreEquivalent(new[] { "a", "b", "c" }, model);
}
Code language: C# (cs)

Note: Be sure to add ‘async Task’ to the unit test method signature. This allows you to await ReadRequestBodyAsync().

To be clear, ReadRequestBodyAsync() returns an InputFormatterResult object. To check the model object value, you need to first cast InputFormatterResult.Model to the model type (string[] in this case).

3 – Add a unit test for the error scenario

The ReadRequestBodyAsync() method we’re testing returns an error and populates the ModelState when the content starts with “bad”. Here’s how to unit test this error scenario:

[TestMethod()]
public async Task TestReadBody_WhenInvalidBodyContent_ReturnsError()
{
	//arrange
	var csvStringInputFormatter = new CsvStringInputFormatter();

	string content = "bad,a,b,c";
	string contentType = "text/csv";
	Type modelType = typeof(string[]);

	var inputFormatterContext = BuildContext(content, contentType, modelType);

	//act
	var result = await csvStringInputFormatter.ReadRequestBodyAsync(inputFormatterContext);

	//assert
	
	Assert.IsTrue(result.HasError);
	Assert.IsTrue(inputFormatterContext.ModelState.ContainsKey("Body"));
	var errorMessage = inputFormatterContext.ModelState["Body"].Errors.First().ErrorMessage;
	Assert.AreEqual("Invalid value in CSV!", errorMessage);
}
Code language: C# (cs)

To check if an error happened, check InputFormatterResult.HasError.

For a deeper check, you can verify that the ModelState (in the context) contains the error message. The data in ModelState is used by the framework to generate the problem details error response (400 – Bad Request). Since this is just a unit test, you don’t have the actual error response, and can just verify that the ModelState has the error message.

Leave a Comment