Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit Test Windows.Web.Http HttpClient with mocked IHttpFilter and IHttpContent, MockedHttpFilter throws System.InvalidCastException

I have a class that depends on the HttpClient from Windows.Web.Http (Windows 10 UAP App). I want to unit test and therefore I need to "mock" the HttpClient to setup what a Get-Call should return. I started with a "simple" unit test with a HttpClient using a handwritten-mocked IHttpFilter and IHttpContent. It's not working as expected and I get a InvalidCastException in the Test-Explorer.

The unit test looks like:

    [TestMethod]
    public async Task TestMockedHttpFilter()
    {
        MockedHttpContent mockedContent = new MockedHttpContent("Content from MockedHttpContent");
        MockedHttpFilter mockedHttpFilter = new MockedHttpFilter(HttpStatusCode.Ok, mockedContent);

        HttpClient httpClient = new HttpClient(mockedHttpFilter);
        var resultContentTask = await httpClient.SendRequestAsync(new HttpRequestMessage(HttpMethod.Get, new Uri("http://dontcare.ch"))).AsTask().ConfigureAwait(false);
        // Test stops here, throwing System.InvalidCastException: Specified cast is not valid

        // Code not reached...
        var result = await resultContentTask.Content.ReadAsStringAsync();
        Assert.AreEqual("Content from MockedHttpContent", result);
    }

I implemented IHttpFilter in MockedHttpFilter:

public class MockedHttpFilter : IHttpFilter
{
    private HttpStatusCode _statusCode;
    private IHttpContent _content;

    public MockedHttpFilter(HttpStatusCode statusCode, IHttpContent content)
    {
        _statusCode = statusCode;
        _content = content;
    }

    public IAsyncOperationWithProgress<HttpResponseMessage, HttpProgress> SendRequestAsync(HttpRequestMessage request)
    {
        return AsyncInfo.Run<HttpResponseMessage, HttpProgress>((token, progress) =>
        Task.Run<HttpResponseMessage>(()=>
        {
            HttpResponseMessage response = new HttpResponseMessage(_statusCode);
            response.Content = _content;
            return response; // Exception thrown after return, but not catched by code/debugger...
        }));
    }
}

I implemented IHttpContent in MockedHttpContent:

public class MockedHttpContent : IHttpContent
{
    private string _contentToReturn;

    public MockedHttpContent(string contentToReturn)
    {
        _contentToReturn = contentToReturn;
    }

    public HttpContentHeaderCollection Headers
    {
        get
        {
            return new HttpContentHeaderCollection();
        }
    }

    public IAsyncOperationWithProgress<string, ulong> ReadAsStringAsync()
    {
        return AsyncInfo.Run<string, ulong>((token, progress) => Task.Run<string>(() =>
        {
            return _contentToReturn;
        }));
    }
}

The error in the Test-Explorer result view:

Test Name:  TestMockedHttpFilter
Test FullName:  xxx.UnitTests.xxxHttpClientUnitTests.TestMockedHttpFilter
Test Source:    xxx.UnitTests\xxxHttpClientUnitTests.cs : line 22
Test Outcome:   Failed
Test Duration:  0:00:00.1990313

Result StackTrace:  
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
   at xxx.UnitTests.xxxHttpClientUnitTests.<TestMockedHttpFilter>d__1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
Result Message: Test method xxx.UnitTests.xxxHttpClientUnitTests.TestMockedHttpFilter threw exception: 
System.InvalidCastException: Specified cast is not valid.

First, not sure why the exception is thrown / what I'm doing wrong. Maybe someone can point me in the right direction or give a hint what to check / test next?

Second, is there a better way to unit test code with a HttpClient dependency (Windows 10 UAP)?

like image 297
1ppCH Avatar asked Sep 29 '15 11:09

1ppCH


1 Answers

You're unit test doesn't make sense, you aren't really testing your class, but instead you are attempting to test the HttpClient class. I'm going to assume a few things since we don't have the method under test in your question; I'm assuming that you do something like the following:

public async Task<MyCustomClass> MethodUnderTest()
{
    // ...

    using (var client = new HttpClient(...))
    {
        // ...

        var json = response.Content.ReadAsStringAsync();
        return JsonConvert.Deserialize<MyCustomClass>(json);
    }
}

If this is the case, then you have to understand that your class does not accept certain dependencies. You can either alter the class so that every external dependency is injected, but that may be a slight overkill... do you really want anyone who consumes and instantiates your class to have to supply an HttpClient to you? So, the better alternative is to use a mocking framework which can mock a dependency of a concrete class; most mocking frameworks can handle mocking interfaces (those are easy), but very few can handle mocking concrete classes.

The framework I would suggest to use is the Microsoft Fakes framework. Microsoft Fakes supports Shims (Isolation) and Stubs (Mocks). Isolation is what you need in order to control member calls of concrete classes.

So, given my example, which members need to be controlled?

  1. HttpResponseMessage.Content_Get
  2. HttpContent.ReadAsStringAsync

I don't think you need to alter the behavior of the JsonConvert.Deserialize<T>() member, but you could if you wanted to. The Microsoft Fakes framework is a bit daunting at first, but once you start using it and get it to work, it'll become easier to use. Alternatively, you can use other frameworks which support Isolation:

  • JustMock from Telerik
  • TypeMock Isolator

Maybe others exist, I'm not familiar with them.

like image 184
myermian Avatar answered Sep 23 '22 06:09

myermian