2

I'm using Flurl Http to make http requests. In the unit tests, I'm trying to verify that the expected content was passed to the sender. I'm trying it like:

httpTest.ShouldHaveCalled(url)
        .WithVerb(HttpMethod.Post)
        .WithContentType(contentType)
        .With(w => w.Request.Content.ReadAsStringAsync().Result == content)
        .Times(1);

However, this fails with System.ObjectDisposedException Cannot access a disposed object. Object name: 'System.Net.Http.StringContent'.

It looks like Flurl is disposing the request body content before the verification is done in the test. How can I capture the request body for verification?

EDIT (A fully reproducible example):

using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

using Autofac.Extras.Moq;

using Flurl.Http;
using Flurl.Http.Testing;

using Xunit;

namespace XUnitTestProject1
{
    public class MyClassTest : IDisposable
    {
        private readonly AutoMock container;
        private readonly HttpTest client;

        public MyClassTest()
        {
            this.container = AutoMock.GetLoose();
            this.client = new HttpTest();
        }

        [Fact]
        public async Task SendAsync_ValidateRequestBody()
        {
            const string url = "http://www.example.com";
            const string content = "Hello, world";

            var sut = this.container.Create<MyClass>();
            await sut.SendAsync(url, content);

            this.client.ShouldHaveCalled(url)
                .With(w => w.Request.Content.ReadAsStringAsync().Result == content);
        }

        public void Dispose()
        {
            this.container?.Dispose();
            this.client?.Dispose();
        }
    }

    public class MyClass
    {
        public virtual async Task SendAsync(string url, string content)
        {
            await url.PostAsync(new StringContent(content, Encoding.UTF8, "text/plain"));
        }
    }
}
kovac
  • 4,945
  • 9
  • 47
  • 90

1 Answers1

4

In most cases (see edit below), Flurl has captured it, you just have to access it differently.

In your example, w.Request is a "raw" HttpRequestMessage, from the HttpClient stack, that Flurl exposes so you can get under the hood if you need to. HttpRequestMessage.Content is a read-once, forward-only stream that has already been read and disposed by the time you're accessing it.

To assert the captured string body, you would typically just do this instead:

httpTest.ShouldHaveCalled(url)
    ...
    .WithRequestBody(content)

EDIT

As you noted, this doesn't work based on how you're using Flurl. The string contained by StringContent is effectively write-only, i.e. no property exposes it for reading. This is the purpose of Flurl's CapturedStringContent. If you use that type as a direct replacement for StringContent, RequestBody will be available in your test.

The reason this isn't very well covered in the docs is because if you do things "the Flurl way", you're not explicitly creating content objects in the first place. PostStringAsync and PostJsonAsync are the far more common ways to send a POST request, and both are implemented using CapturedStringContent. Use one of those methods if you can, or use PostAsync(new CapturedStringContent(...)) if you need to get at the lower-level content object for some reason.

Todd Menier
  • 37,557
  • 17
  • 150
  • 173
  • This is actually what I tried initially, but the `x.RequestBody` is always null. I guess this has something to do with the code documentation that says `Available ONLY if Request.Content is a Flurl.Http.Content.CapturedStringContent`. Not sure what that actually means, but the above suggestion as it is, does not work. – kovac Nov 16 '19 at 03:56
  • Edited, and I forgot that asserting the request body (when possible) is easier than I originally posted :) – Todd Menier Nov 16 '19 at 17:44
  • Sorry but `.WithRequestBody(content)` doesn't work either. Test is failing when it shouldn't. You can try the code I pasted, it's self-contained and should be able to reproduce the issue. – kovac Nov 17 '19 at 08:29
  • I don't think you read my whole edit. Avoid creating a `StringContent` and use one of the alternatives I suggested. Otherwise it won't work. – Todd Menier Nov 17 '19 at 14:18
  • 1
    Apologies, my mistake. It's working with `CapturedStringContent`. – kovac Nov 18 '19 at 03:02