2

I am working on a project using WebApi2. With my test project I am using Moq and XUnit.

So far testing an api has been pretty straight forward to do a GET like

  [Fact()]
    public void GetCustomer()
    {
        var id = 2;

        _customerMock.Setup(c => c.FindSingle(id))
            .Returns(FakeCustomers()
            .Single(cust => cust.Id == id));

        var result = new CustomersController(_customerMock.Object).Get(id);

        var negotiatedResult = result as OkContentActionResult<Customer>;
        Assert.NotNull(negotiatedResult);
        Assert.IsType<OkNegotiatedContentResult<Customer>>(negotiatedResult);
        Assert.Equal(negotiatedResult.Content.Id,id);
    }

Now I am moving onto something a little complicated where I need to access value from the request header.

I have created my own Ok() result by extending the IHttpActionResult

   public OkContentActionResult(T content,HttpRequestMessage request)
    {
        _request = request;
        _content = content;
    }

This allows me to have a small helper that reads the header value from the request.

 public virtual IHttpActionResult Post(Customer customer)
    {
        var header = RequestHeader.GetHeaderValue("customerId", this.Request);

        if (header != "1234")

How am I meant to setup Moq with a dummy Request?

I have spent the last hour or so hunting for an example that allows me to do this with webapi however I cant seem to find anything.

So far.....and I am pretty sure its wrong for the api but I have

      // arrange
        var context = new Mock<HttpContextBase>();
        var request = new Mock<HttpRequestBase>();
        var headers = new NameValueCollection
        {
            { "customerId", "111111" }
        };
        request.Setup(x => x.Headers).Returns(headers);
        request.Setup(x => x.HttpMethod).Returns("GET");
        request.Setup(x => x.Url).Returns(new Uri("http://foo.com"));
        request.Setup(x => x.RawUrl).Returns("/foo");
        context.Setup(x => x.Request).Returns(request.Object);
        var controller = new Mock<ControllerBase>();
        _customerController = new CustomerController()
        {
            //  Request = request,

        };

I am not really sure what next I need to do as I havent needed to setup a mock HttpRequestBase in the past.

Can anyone suggest a good article or point me in the right direction?

Thank you!!!

1 Answer 1

7

I believe that you should avoid reading the headers in your controller for better separation of concerns (you don't need to read the Customer from request body in the controller right?) and testability.

How I will do it is create a CustomerId class (this is optional. see note below) and CustomerIdParameterBinding

public class CustomerId
{
    public string Value { get; set; }
}

public class CustomerIdParameterBinding : HttpParameterBinding
{
    public CustomerIdParameterBinding(HttpParameterDescriptor parameter) 
    : base(parameter)
    {
    }

    public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
    {
        actionContext.ActionArguments[Descriptor.ParameterName] = new CustomerId { Value = GetIdOrNull(actionContext) };
        return Task.FromResult(0);
    }

    private string GetIdOrNull(HttpActionContext actionContext)
    {
        IEnumerable<string> idValues;
        if(actionContext.Request.Headers.TryGetValues("customerId", out idValues))
        {
            return idValues.First();
        }
        return null;
    }
}

Writing up the CustomerIdParameterBinding

config.ParameterBindingRules.Add(p =>
{
    return p.ParameterType == typeof(CustomerId) ? new CustomerIdParameterBinding(p) : null;
});

Then in my controller

public void Post(CustomerId id, Customer customer)

Testing the Parameter Binding

public void TestMethod()
{
    var parameterName = "TestParam";
    var expectedCustomerIdValue = "Yehey!";

    //Arrange
    var requestMessage = new HttpRequestMessage(HttpMethod.Post, "http://localhost/someUri");
    requestMessage.Headers.Add("customerId", expectedCustomerIdValue );

    var httpActionContext = new HttpActionContext
    {
        ControllerContext = new HttpControllerContext
        {
            Request = requestMessage
        }
    };

    var stubParameterDescriptor = new Mock<HttpParameterDescriptor>();
    stubParameterDescriptor.SetupGet(i => i.ParameterName).Returns(parameterName);

    //Act
    var customerIdParameterBinding = new CustomerIdParameterBinding(stubParameterDescriptor.Object);
    customerIdParameterBinding.ExecuteBindingAsync(null, httpActionContext, (new CancellationTokenSource()).Token).Wait();

    //Assert here
    //httpActionContext.ActionArguments[parameterName] contains the CustomerId
}

Note: If you don't want to create a CustomerId class, you can annotate your parameter with a custom ParameterBindingAttribute. Like so

public void Post([CustomerId] string customerId, Customer customer)

See here on how to create a ParameterBindingAttribute

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks @LostInComputer for your detailed response. I do agree the api controller feels a dirty place to validate the header. However part of our api is going to require the user provides a customerid within the header. I was thinking of using an actionfilter to validate this however I am still keen to be able to test this. customerid is the first of about 4 values being kept in the header we need to test.
My suggested solution is you can separate the tests into two. 1: Test that a valid customerId is provided to the controller 2. Test that the customerId is retrieved from the header by the parameter binding.
Hopefully, we get another answer from somebody. I'm also curious for other ideas.
When you add the parameter-binding rules to httpconfiguration, you supplied a lambda function, which fits the definition according to the documentation, learn.microsoft.com/en-us/previous-versions/aspnet/…. However, isn't there a type that needs to be supplied? Yet, the compiler just give a free pass. Why?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.