Unable to mock Owin context in C# WEB API REST ser

2019-02-25 04:54发布

问题:

I am trying to mock the OWIN context in a C# WEB API REST service but the OWIN context always seems to be null. I am using moq and .NET framework 4.5.2.

Here is the controller method I am trying to test:

public class CustomerController : ApiController
{
    // GET: api/Customer/n
    [HttpGet]
    [ClaimsPrincipalPermission(SecurityAction.Demand, Operation = Actions.View, Resource = Resources.Self)]
    public IHttpActionResult Get(int id)
    {
        if (id <= 0)
            return BadRequest();

        ClaimsPrincipalPermission.CheckAccess(id.ToString(),"UserId");

        string jsonResponse = string.Empty;
        HttpStatusCode returnCode = this.ForwardGetRequestToWallet(String.Format("customer/gethistory/{0}", id), out jsonResponse);
        if (returnCode== HttpStatusCode.OK)
            return Ok(jsonResponse);
        return StatusCode(returnCode);            
    } 

And in a helper class the code that goes wrong, on the controller.Request.GetOwinContext call:

public static HttpClient GetClient(this ApiController controller)
{
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
    client.DefaultRequestHeaders.Authorization = controller.Request.Headers.Authorization;
    var test = controller.Request.GetOwinContext().Request.RemoteIpAddress;
    return client;
} 

My test initialize:

[TestInitialize()]
public void MyTestInitialize() 
{
    player_user_id = Convert.ToInt32(ConfigurationManager.AppSettings["PLAYER_USER_ID"]);
    player_user_name = ConfigurationManager.AppSettings["PLAYER_USER_NAME"];
    API_protocol = ConfigurationManager.AppSettings["WEB_API_PROTOCOL"];
    API_host = ConfigurationManager.AppSettings["WEB_API_HOST"];
    API_port = ConfigurationManager.AppSettings["WEB_API_PORT"];

    wrapper = new ClaimsPrincipalWrapper();

    server = new Mock<HttpServerUtilityBase>();
    response = new Mock<HttpResponseBase>();

    request = new Mock<HttpRequestBase>();

    session = new Mock<HttpSessionStateBase>();
    session.Setup(s => s.SessionID).Returns(Guid.NewGuid().ToString());

    config = new HttpConfiguration();

    owinContext = new Mock<IOwinContext>();

    identity = new GenericIdentity(player_user_name);
    identity.AddClaim(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", player_user_id.ToString()));

    principal = new GenericPrincipal(identity, new[] { "User" });

    context = new Mock<HttpContextBase>();
    context.SetupGet(c => c.Request).Returns(request.Object);
    context.SetupGet(c => c.Response).Returns(response.Object);
    context.SetupGet(c => c.Server).Returns(server.Object);
    context.SetupGet(c => c.Session).Returns(session.Object);
    context.SetupGet(p => p.User.Identity.Name).Returns(player_user_name);
    context.SetupGet(p => p.Request.IsAuthenticated).Returns(true);
    context.SetupGet(s => s.User).Returns(principal);

}

My test:

[TestMethod]
public void Get()
{
    var queryMock = new Mock<IReadableStringCollection>();

    var requestMock = new Mock<IOwinRequest>();

    requestMock.Setup(m => m.Query).Returns(queryMock.Object);

    owinContext.Setup(m => m.Request).Returns(requestMock.Object);

    testURI = new Uri(String.Format("{0}://{1}:{2}/Get/{3}", API_protocol, API_host, API_port, player_user_id));
    request.Setup(r => r.Url).Returns(testURI);

    message = new HttpRequestMessage(HttpMethod.Get, testURI);
    var route = config.Routes.MapHttpRoute("Default", "api/{controller}/{id}");
    var routeData = new HttpRouteData(route, new HttpRouteValueDictionary { { "customerController", "get" } });

    var testControllerContext = new HttpControllerContext(config, routeData, message);

    var controller = new SharedWalletAPI.Controllers.CustomerController()
    {
        ControllerContext = testControllerContext,
        Request = new HttpRequestMessage(HttpMethod.Get, testURI),
        User = new System.Security.Principal.GenericPrincipal(identity, new string[0])
    };

    // Act
    var _result = controller.Get(player_user_id);

    // get JSON
    //CustomerHistoryList jsonObj = JsonConvert.DeserializeObject<CustomerHistoryList>(_result);

    // Assert
    Assert.IsNotNull(_result);
}

All seems fine until we actually do the get then the OWIN GetOwinContext call which always returns null and hence throws an error of the old chestnut type "Object Reference not set to an instance of an object".

I am still pretty new to OWIN. Can anyone advise me here?

回答1:

First off, I see where you've setup your mock of IOwinContext, but none of the code that you've shown has actually tied your mock into the request. Specifically, there's nothing setup that causes controller.Request.GetOwinContext() to return your mocked object. So, since you haven't setup an expectation for that call, it will return null.

Testing

The real pain that you're running into is from trying to Mock someone else's very wide interfaces (plural). That's always a painful thing to do. As evidence, look at the amount of code you've had to write to initialize and execute your test. I would try and hide the access to these things behind interfaces that are much more focused and easy to mock / stub / dummy.

As much of a unit testing zealot as I am, I don't ever test my WebAPI or MVC controllers in isolation. They almost always depend on external things that I don't want to dummy up like HttpContext, authentication concerns, method-level attributes. Instead, I keep the controllers as thin as possible, mostly limiting the methods to marshalling things in and out of the context, etc. All of the business logic gets moved into classes that the controllers depend on. Then, those classes are easier to test because they don't depend on things that are a pain to fake.

I still write tests that exercise the controller methods, and generally they cover every branch of the controllers. It's just that these are system-level functional tests like automated browser tests for MVC apps or tests that hit the API using an HttpClient for WebAPI apps. If the controllers are kept thin, then there isn't much there to get test coverage on in the first place.