11

I'm trying to get a call to WriteAsync mocked on a mockHttpResponse and I can't figure out the syntax to use.

var responseMock = new Mock<HttpResponse>();
responseMock.Setup(x => x.WriteAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()));

ctx.Setup(x => x.Response).Returns(responseMock.Object);

The test bombs with the following error:

System.NotSupportedException : Invalid setup on an extension method: x => x.WriteAsync(It.IsAny(), It.IsAny())

Ultimately I want to verify the correct string has been written to the response.

How to correctly set this up?

Nkosi
  • 235,767
  • 35
  • 427
  • 472
Jammer
  • 9,969
  • 11
  • 68
  • 115

3 Answers3

10

Here's a solution that seems to work in .NET Core 3.1, for completeness:

const string expectedResponseText = "I see your schwartz is as big as mine!";

DefaultHttpContext httpContext = new DefaultHttpContext();
httpContext.Response.Body = new MemoryStream();

// Whatever your test needs to do

httpContext.Response.Body.Position = 0;
using (StreamReader streamReader = new StreamReader(httpContext.Response.Body))
{
    string actualResponseText = await streamReader.ReadToEndAsync();
    Assert.Equal(expectedResponseText, actualResponseText);
}
infl3x
  • 1,492
  • 19
  • 26
9

Moq cannot Setup extension methods. If you know what the extension method accesses then some cases you can mock a safe path through the extension method.

WriteAsync(HttpResponse, String, CancellationToken)

Writes the given text to the response body. UTF-8 encoding will be used.

directly accesses the HttpResponse.Body.WriteAsync where Body is a Stream via the following overload

/// <summary>
/// Writes the given text to the response body using the given encoding.
/// </summary>
/// <param name="response">The <see cref="HttpResponse"/>.</param>
/// <param name="text">The text to write to the response.</param>
/// <param name="encoding">The encoding to use.</param>
/// <param name="cancellationToken">Notifies when request operations should be cancelled.</param>
/// <returns>A task that represents the completion of the write operation.</returns>
public static Task WriteAsync(this HttpResponse response, string text, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken))
{
    if (response == null)
    {
        throw new ArgumentNullException(nameof(response));
    }

    if (text == null)
    {
        throw new ArgumentNullException(nameof(text));
    }

    if (encoding == null)
    {
        throw new ArgumentNullException(nameof(encoding));
    }

    byte[] data = encoding.GetBytes(text);
    return response.Body.WriteAsync(data, 0, data.Length, cancellationToken);
}

This means you would need mock response.Body.WriteAsync

//Arrange
var expected = "Hello World";
string actual = null;
var responseMock = new Mock<HttpResponse>();
responseMock
    .Setup(_ => _.Body.WriteAsync(It.IsAny<byte[]>(),It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
    .Callback((byte[] data, int offset, int length, CancellationToken token)=> {
        if(length > 0)
            actual = Encoding.UTF8.GetString(data);
    })
    .ReturnsAsync();

//...code removed for brevity

//...
Assert.AreEqual(expected, actual);

The callback was used to capture the arguments passed to the mocked member. Its value was stored in a variable to be asserted later in the test.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • 2
    This was a great answer but unfortunately doesn't work with .NET Core 3.1 - the extension methods became a lot more complicated. I would appreciate any guidance... thanks. – Steven Darby Jan 16 '20 at 21:47
  • @StevenDarby I'll have to go take a look at the new version to figure out what changed. Do you have a link to the new source, to save me hunting it down? – Nkosi Jan 16 '20 at 21:52
  • @StevenDarby Ok found the new source. Checking it out. – Nkosi Jan 16 '20 at 21:55
  • 1
    @StevenDarby Yes things have become way more complicated in more recent version. Try using a `DefaultHttpContext` instead and modifying its response to suit your test case. – Nkosi Jan 16 '20 at 22:18
  • Thank you - that's what I did in the end. Found I didn't need a mock in this particular case. Thanks for taking a look. – Steven Darby Jan 30 '20 at 17:56
  • 1
    Usage of DefaultHttpContext with .NET Core 3.1 worked for me as well. The only thing is setup of Response.Body with MemoryStream (https://stackoverflow.com/questions/45959605/inspect-defaulthttpcontext-body-in-unit-test-situation), since I have assertion of body content in my unit tests – Pylyp Lebediev Feb 28 '20 at 14:51
2

With .Net 6 it was such a pain I ended up using a real implementation.

    // It's a PAIN to unit test Response.WriteAsync, just use a Real HttpContext 
    //_httpContextMock.Setup(x => x.Request.Path).Returns(new PathString("/api/test"));
    //_httpContextMock.Setup(x => x.Connection.Id).Returns("1");
    //_httpContextMock.Setup(x => x.Request.Headers).Returns(new HeaderDictionary() { { RequestLoggingMiddleware.TraceIdHeaderName, new[] { invalidTraceId } } });
    //_httpContextMock.Setup(x => x.Response.Body).Returns(new MemoryStream());  //<- PAIN TO TEST
    //_httpContextMock.SetupProperty(x => x.Response.ContentType);
    //_httpContextMock.SetupProperty(x => x.Response.StatusCode);
    //_requestDelegateMock.Setup(x => x(_httpContextMock.Object)).Returns(Task.CompletedTask);

    var context = new DefaultHttpContext();
    context.Response.Body = new MemoryStream();
    context.Request.Headers.Add(new KeyValuePair<string, StringValues>(traceIdHeaderName, invalidTraceId));
    
    // Act
    await _middleware.InvokeAsync(context, _loggingContextMock.Object);
Jeremy Thompson
  • 61,933
  • 36
  • 195
  • 321