I tried to mock the HttpContext for unit tests for a .net 6 web app. What's interesting is that I can get any files added to the FileCollection. However, if I try to access the form fields, it throws an Object reference not set to an instance of an object exception. Here's the minimal test code.
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
using Moq;
using NUnit.Framework;
using TestWebApp.Controllers;
namespace TestDemo;
[TestFixture]
public class TestControllerFixture
{
[Test]
public void returns_sum()
{
var content = "Hello World from a Fake File";
var fileName = "test.pdf";
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(content);
writer.Flush();
stream.Position = 0;
IFormFile file = new FormFile(stream, 0, stream.Length, "fileId", fileName);
var _httpContextAccessor = new MockHttpContextAccessor();
_httpContextAccessor.AddFormFieldsOrFile(new Dictionary<string, StringValues>()
{
{"expense-ids", new StringValues(new[] {"1","2","3"})}
}, new List<IFormFile>() {file});
var controller = new TestController(_httpContextAccessor.ContextAccessor.Object);
var result = controller.Test();
Assert.AreEqual(3, result);
}
}
public class MockHttpContextAccessor
{
public Mock<IHttpContextAccessor> ContextAccessor { get; private set; }
private FormFileCollection fileCollection;
private FormCollection requestForm;
public MockHttpContextAccessor()
{
var requestFeature = new HttpRequestFeature();
var features = new FeatureCollection();
features.Set<IHttpRequestFeature>(requestFeature);
ContextAccessor = new Mock<IHttpContextAccessor>();
var session = new MockHttpSession();
var context = new DefaultHttpContext(features);
fileCollection = new FormFileCollection();
requestForm = new FormCollection(new Dictionary<string, StringValues>(), fileCollection);
ContextAccessor.Setup(x => x.HttpContext).Returns(context);
ContextAccessor.Setup(x => x.HttpContext.Session).Returns(session);
ContextAccessor.Setup(x => x.HttpContext.Request.Path).Returns("/path");
ContextAccessor.Setup(x => x.HttpContext.Request.Form).Returns(requestForm);
ContextAccessor.Setup(x => x.HttpContext.Request.Form.Files).Returns(requestForm.Files);
ContextAccessor.Setup(x => x.HttpContext.Request.Headers).Returns(new Mock<IHeaderDictionary>().Object);
context.Request.Form = requestForm;
}
public void AddRequestBody(string content)
{
ContextAccessor.Setup(x => x.HttpContext.Request.Body)
.Returns(new MemoryStream(System.Text.Encoding.UTF8.GetBytes(content)));
}
public void AddFormFieldsOrFile(Dictionary<string, StringValues> fields, List<IFormFile> files = null)
{
if (files != null && files.Any())
{
ContextAccessor.Object.HttpContext.Request.Headers.Add("Content-Type", "multipart/form-data");
fileCollection.AddRange(files);
}
requestForm = new FormCollection(fields, fileCollection);
}
public void ClearFormFiles()
{
fileCollection.Clear();
}
}
public class MockHttpSession : ISession
{
Dictionary<string, object> sessionStorage = new Dictionary<string, object>();
public object this[string name]
{
get { return sessionStorage[name]; }
set { sessionStorage[name] = value; }
}
string ISession.Id
{
get { throw new NotImplementedException(); }
}
bool ISession.IsAvailable
{
get { throw new NotImplementedException(); }
}
IEnumerable<string> ISession.Keys
{
get { return sessionStorage.Keys; }
}
void ISession.Clear()
{
sessionStorage.Clear();
}
void ISession.Remove(string key)
{
sessionStorage.Remove(key);
}
void ISession.Set(string key, byte[] value)
{
sessionStorage[key] = value;
}
public Task LoadAsync(CancellationToken cancellationToken = new CancellationToken())
{
throw new NotImplementedException();
}
public Task CommitAsync(CancellationToken cancellationToken = new CancellationToken())
{
throw new NotImplementedException();
}
bool ISession.TryGetValue(string key, out byte[] value)
{
object storageValue = null;
if (sessionStorage.TryGetValue(key, out storageValue))
{
var bytes = storageValue as byte[];
if (bytes != null)
{
value = bytes;
return true;
}
value = Encoding.UTF8.GetBytes(storageValue.ToString());
return true;
}
value = null;
return false;
}
}
This is the demo controller method being tested. The file is found, but not the form field.
using Microsoft.AspNetCore.Mvc;
namespace TestWebApp.Controllers;
public class TestController : Controller
{
private IHttpContextAccessor _httpContextAccessor;
public TestController(IHttpContextAccessor context)
{
_httpContextAccessor = context;
}
[HttpPost("[action]")]
public IActionResult Test()
{
var form = _httpContextAccessor.HttpContext.Request.Form;
var file = form.Files[0];
var ids = form["expense-ids"].ToString().Split(',')
.Select(int.Parse).ToList();
var total = ids.Sum();
return Json(new {total});
}
}
And here's a screenshot of the mocked object. In it, you can see there's 1 file in the Files collection. The stack trace of the exception only provides this.
at System.Collections.Generic.LargeArrayBuilder`1.AddRange(IEnumerable`1 items)
at System.Collections.Generic.EnumerableHelpers.ToArray[T](IEnumerable`1 source)
at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
at System.Linq.SystemCore_EnumerableDebugView`1.get_Items()
