How can I Mock HttpResponseMessage SendAsync in VB.NET?

168 views Asked by At

I have found literally a hundred posts on how to do this in C#, but not a one on how to do it in VB.Net. Every attempt to translate these methods into VB.NET has failed. It seems to boil down to the fact that the SendAsync is a "Protected Friend", as shown in the following error image: <removed at the direction of Mark Seeman - see below>

If you can't see the image, the error I'm getting is: 'HttpMessageHandler.Protected Friend MustOverride Overloads Function SendAsync(request As HttpRequestMessage, candellationToken As CancellationToken) As Task(Of HttpResponseMessage)' is not accessible in this context because it is 'Protected Friend'. The error number is BC30390, which leads to the exceedingly helpful Microsoft error page "Sorry, we don't have specifics on this Visual Basic error".

Firstly, this is a behemoth of an application crossing numerous divisions, and changing to C# is not an option. I am unit testing a section of business logic that calls an API from another system and then, upon return of that data, executes additional code and returns the result. The test data cannot count on results returned from the external API, so I need to mock the API result to match with my test data. Here is that portion of the BL Function that I am attempting to test:

    Public Sub New(context As IUSASContext, account As IAccount, configurationManager As IDBConfigurationManager)
        _Context = context
        _Account = account
        _AppSettingsConfigManager = configurationManager
        _UserPreferenceHistory = New UserPreferenceHistory(_Context, _Account)
    End Sub


    Public Async Function GetUserPreferenceResponseData(id As Long, httpClient As HttpClient) As Task(Of UserPreferences)
        Dim baseUri = New Uri(AppSettingsHelper.GetValue(AppSettingName.ActivitySummaryApiUri, "", _AppSettingsConfigManager))
        Dim userContentReponse = Await httpClient.GetAsync(New Uri(baseUri, "/api/v1/UserContents"))
        Dim userContentData = Await userContentReponse.Content.ReadAsStringAsync()

        Dim userResponse = Await httpClient.GetAsync(New Uri(baseUri, $"/api/v1/Users/{_Account.CurrentTenant}/{id}"))
        Dim userData = Await userResponse.Content.ReadAsStringAsync()

        Dim userPreferences As UserPreferences = Newtonsoft.Json.JsonConvert.DeserializeObject(Of UserPreferences)(userData)
        userPreferences.SchedulesList = GetUserPreferencesEmailScheduleList()
        userPreferences.UserContentsList = Newtonsoft.Json.JsonConvert.DeserializeObject(Of List(Of Opm.Staffing.Models.ActivitySummary.UserContent))(userContentData)
        userPreferences.Id = id

        Return userPreferences
    End Function

First, I tried to Mock the HttpClient itself, and setting up the "GetAsync" function, which proved impossible. A quick web search proved that the general consensus was to, instead, mock the HttpResponseMessage and setup the "SendAsync". However, the C# examples show to put "Protected" prior to the "Setup" keyword, as in the following example:

mockHttpMessageHandler.Protected()
                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
                .ReturnsAsync(new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent("{'name':thecodebuzz,'city':'USA'}"),
                });

When I try to do that, I get the following error: <removed at the direction of Mark Seeman - see below> If you can't see the error, the text is: "Lambda expression cannot be converted to 'String' because 'String' is not a delegate type."

Here is the latest version of my unit test:

        <TestMethod()> Public Sub GetUserPreferenceResponseData()

        'Arrange
        Dim methodName = System.Reflection.MethodBase.GetCurrentMethod().Name

        Dim asuc As New ActivitySummary.UserContent() With {.UserContentId = 1, .UserContentName = "BB"}
        Dim uc As New List(Of ActivitySummary.UserContent) From {asuc}

 ' the below produces the "Protected Friend" intellesense error
 '_HTTPMessageHandlerMock.Setup(Function(x) x.SendAsync(It.IsAny(Of HttpRequestMessage), It.IsAny(Of Threading.CancellationToken))) _
 .Returns(New HttpResponseMessage With {.Content = New StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(uc))})

 ' the below produces the "String not a Delegate Type " intellesense error
 _HTTPMessageHandlerMock.Protected().Setup(Function(x) x.SendAsync(It.IsAny(Of HttpRequestMessage), It.IsAny(Of Threading.CancellationToken))) _
 .Returns(New HttpResponseMessage With {.Content = New StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(uc))})

        Dim _Client = New HttpClient(_HTTPMessageHandlerMock.Object)

        Dim userPreferencesBL As New UserPreferencesBL(_Context, _Account, _dbConfigManager)
        Dim uid As Integer

        'Action
        Dim result = userPreferencesBL.GetUserPreferenceResponseData(uid, _Client)

        'Assert
        Assert.IsNotNull(result, methodName + " returned null, expected User Preferences.")

    End Sub

Mousing over "SendAsync" shows the error I mentioned above.

I keep going around in circles, as changing one thing will break another that I had thought I had fixed. Any help would be greatly appreciated.

2

There are 2 answers

0
Mark Seemann On

Something like this ought to work:

Dim httpMessageHandlerMock As New Mock(Of HttpMessageHandler)
httpMessageHandlerMock _
    .Protected() _ ' Needs Imports Moq.Protected
    .Setup(Of Task(Of HttpResponseMessage))(
        "SendAsync",
        ItExpr.IsAny(Of HttpRequestMessage),
        ItExpr.IsAny(Of Threading.CancellationToken)) _
    .ReturnsAsync(
        New HttpResponseMessage() With
        {
            .Content = New JsonConvert.SerializeObject(uc))
        })

Dim client = New HttpClient(httpMessageHandlerMock.Object)

Notice that, as the comment says, you need

Imports Moq.Protected

because otherwise the Protected extension method and the rest of that API isn't available.

You can't use lambda expressions or other strongly typed expressions with Protected access, because the method is only visible to inheriting classes, and the test, or httpMessageHandlerMock, isn't inheriting from HttpMessageHandler. Therefore, you instead need to identify the method you want to override with a string ("SendAsync") and use the ItExpr API to match on input arguments.

1
Perringaiden On

As someone doing similar testing in VB.NET with HttpClient, I've implemented my own DelegatingHandler which overrides the SendAsync to "handle" the request and generate the controlled content.

I'll post a simplified version of what I'm doing, but the key point is that the TestDelegatingHandler gets built with a set of handlers based on the method paths to be tested.

Private ReadOnly cgHandlers As Dictionary(Of String, Func(Of HttpRequestMessage, HttpResponseMessage))

''' <summary>
''' Creates a new <see cref="TestDelegatingHandler"/> with custom handlers.
''' </summary>
''' <param name="handlers">
''' The handlers as a Tuple of LocalPath As <see cref="String"/> and Handler
''' As <see cref="Func(Of HttpRequestMesage, HttpResponseMessage)"/>.
''' </param>
Public Sub New(
    ParamArray handlers() As (LocalPath As String, Handler As Func(Of HttpRequestMessage, HttpResponseMessage))
)

    cgHandlers = handlers.ToDictionary(Function(x) x.LocalPath, Function(x) x.Handler)

End Sub

''' <inheritdoc />
Protected Overrides Async Function SendAsync(
        request As HttpRequestMessage,
        cancellationToken As Threading.CancellationToken
    ) As Task(Of HttpResponseMessage)

    Dim rc As HttpResponseMessage
    Dim message As String
    Dim handler As Func(Of HttpRequestMessage, HttpResponseMessage) = Nothing
    Dim localPath As String


    localPath = request.RequestUri.LocalPath

    If cgHandlers.TryGetValue(localPath, handler) Then
        rc = handler.Invoke(request, message)
    Else
        rc = New HttpResponseMessage(Net.HttpStatusCode.NotFound) With {
            .RequestMessage = request
        }
    End If

    Return Await Task.FromResult(rc)
End Function

then the IHttpClientFactory used takes in the handler to create the clients.

Private Shared Function GetHttpClientFactory(delegatingHandler As DelegatingHandler) As IHttpClientFactory
    Dim mock As Mock(Of IHttpClientFactory)


    mock = New Mock(Of IHttpClientFactory)
    mock.Setup(Function(x) x.CreateClient(It.IsAny(Of String)())).Returns(Function() New HttpClient(delegatingHandler))

    Return mock.Object
End Function

Alternatively (based on your code), simply provide the client:

New HttpClient(New TestDelegatingHandler(...handlers...))

It's not going to handle verification through Mock(Of ) style, but you can definitely control the response messages.