Andreas Richter
Andreas Richter

Reputation: 298

How can i unit test an EntitySetController

i try to unit test an EntitySetController. I can test Get but have problems in testing the Post Method.

I played around with SetODataPath and SetODataRouteName but when i call this.sut.Post(entity) i get a lot of errors regarding missing Location Header, missing OData-Path, missing Routes.

I am at my wit's end. Is there anybody out there who has successfully testet their EntitySetController?

Has anybody an idea for me? Maybe should i test only the protected overrided methods from my EntitySetController implementation? But how can i test protected methods?

Thanks for your help

Upvotes: 3

Views: 1449

Answers (5)

lc.
lc.

Reputation: 116528

In addition to everything here, I had to manually attach the context to the request, as well as create route data. Unfortunately there is no way I found to unit-test without a dependency on route/model configuration.

So using a route called "ODataRoute" which is all part of the normal configuration established in my static ODataConfig.Configure() method (same as above, it creates the model and calls a bunch of MapODataServiceRoute), the following code works to prepare a controller for a test:

protected static void SetupControllerForTests(ODataController controller, 
    string entitySetName, HttpMethod httpMethod)
{
    //perform "normal" server configuration
    var config = new HttpConfiguration();
    ODataConfig.Configure(config);

    //set up the request
    var request = new HttpRequestMessage(httpMethod, 
        new Uri(string.Format("http://localhost/odata/{0}", entitySetName)));

    //attach it to the controller
    //note that this will also automagically attach a context to the request!
    controller.Request = request;

    //get the "ODataRoute" route from the configuration
    var route = (ODataRoute)config.Routes["ODataRoute"];

    //extract the model from the route and create a path
    var model = route.PathRouteConstraint.EdmModel;
    var edmEntitySet = model.FindDeclaredEntitySet(entitySetName);
    var path = new ODataPath(new EntitySetPathSegment(edmEntitySet));

    //get a couple more important bits to set in the request
    var routingConventions = route.PathRouteConstraint.RoutingConventions;
    var pathHandler = route.Handler;

    //set the properties of the request
    request.SetConfiguration(config);
    request.Properties.Add("MS_ODataPath", path);
    request.Properties.Add("MS_ODataRouteName", "ODataRoute");
    request.Properties.Add("MS_EdmModel", model);
    request.Properties.Add("MS_ODataRoutingConventions", routingConventions);
    request.Properties.Add("MS_ODataPathHandler", pathHandler);

    //set the configuration in the request context
    var requestContext = (HttpRequestContext)request.Properties[HttpPropertyKeys.RequestContextKey];
    requestContext.Configuration = config;

    //get default route data based on the generated URL and add it to the request
    var routeData = route.GetRouteData("/", request);
    request.SetRouteData(routeData);
}

This took me the better part of a few days to piece together, so I hope this at least saves someone else the same.

Upvotes: 0

mixja
mixja

Reputation: 7467

OK updated answer.

I've also found to support executing a returned IHttpActionResult successfully, a few more things are needed.

Here is the best approach I found so far, I'm sure there is a better way but this works for me:

// Register OData configuration with HTTP Configuration object
// Create an ODataConfig or similar class in App_Start 
ODataConfig.Register(config);

// Get OData Parameters - suggest exposing a public GetEdmModel in ODataConfig
IEdmModel model = ODataConfig.GetEdmModel();
IEdmEntitySet edmEntitySet = model.EntityContainers().Single().FindEntitySet("Orders"); 
ODataPath path = new ODataPath(new EntitySetPathSegment(edmEntitySet));

// OData Routing Convention Configuration
var routingConventions = ODataRoutingConventions.CreateDefault();

// Attach HTTP configuration to HttpRequestContext
requestContext.Configuration = config;

// Attach Request URI
request.RequestUri = requestUri;

// Attach Request Properties
request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, config);
request.Properties.Add(HttpPropertyKeys.RequestContextKey, requestContext);
request.Properties.Add("MS_ODataPath", path);
request.Properties.Add("MS_ODataRouteName", "ODataRoute");
request.Properties.Add("MS_EdmModel", model);
request.Properties.Add("MS_ODataRoutingConventions", routingConventions);
request.Properties.Add("MS_ODataPathHandler", new DefaultODataPathHandler());

Upvotes: 1

mixja
mixja

Reputation: 7467

Also, to get the correct Location header values etc, you really want to call your Web Api application OData configuration code.

So rather than using:

config.Routes.Add("mynameisbob", new MockRoute());

You should separate the portion of the WebApiConfig class that sets up your OData routes into a separate class (e.g. ODataConfig) and use that to register the correct routes for your tests:

e.g.

ODataConfig.Register(config);

The only things you then have to watch out for is that the following lines match your routing configuration:

request.Properties.Add("MS_ODataPath", new ODataPath(new EntitySetPathSegment("MyEntity")));
request.Properties.Add("MS_ODataRouteName", "mynameisbob");

So if your Web API OData configuration is as follows:

    config.Routes.MapODataRoute("ODataRoute", "odata", GetEdmModel());

    private static IEdmModel GetEdmModel()
        {
            ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
            modelBuilder.EntitySet<MyEntity>("MyEntities");
            IEdmModel model = modelBuilder.GetEdmModel();
            return model;
        }

Then this is the correct configuration:

request.Properties.Add("MS_ODataPath", new ODataPath(new EntitySetPathSegment("MyEntities")));
request.Properties.Add("MS_ODataRouteName", "ODataRoute");

With this in place, your Location header will be generated correctly.

Upvotes: 0

mixja
mixja

Reputation: 7467

In addition to the answer from @mynameisbob, I have found you also may need to set the HttpRequestContext as well on the Request properties:

var requestContext = new HttpRequestContext();
requestContext.Configuration = config;
request.Properties.Add(HttpPropertyKeys.RequestContextKey, requestContext);

I needed the above additions for example when creating an HttpResponseMessage as follows:

public virtual HttpResponseException NotFound(HttpRequestMessage request)
    {
        return new HttpResponseException(
            request.CreateResponse(
                HttpStatusCode.NotFound,
                new ODataError
                {
                    Message = "The entity was not found.",
                    MessageLanguage = "en-US",
                    ErrorCode = "Entity Not Found."
                }
            )
        );
    }

Without having the HttpRequestContext set, the above method will throw an Argument Null Exception as the CreateResponse extension method attempts to get the HttpConfiguration from the HttpRequestContext (rather than directly from the HttpRequest).

Upvotes: 1

mynameisbob
mynameisbob

Reputation: 66

Came here looking for a solution aswell. This seems to work however not sure if there is a better way.

The controller needs a minimum of CreateEntity and GetKey overrides:

public class MyController : EntitySetController<MyEntity, int>
{
    protected override MyEntity CreateEntity(MyEntity entity)
    {
        return entity;
    }

    protected override int GetKey(MyEntity entity)
    {
        return entity.Id;
    }
}

Where MyEntity is really simple:

public class MyEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Looks like you need at least: + Request with a URI + 3 keys in the request header, MS_HttpConfiguration, MS_ODataPath and MS_ODataRouteName + A HTTP configuration with a route

[TestMethod]
    public void CanPostToODataController()
    {
        var controller = new MyController();

        var config = new HttpConfiguration();
        var request = new HttpRequestMessage();

        config.Routes.Add("mynameisbob", new MockRoute());

        request.RequestUri = new Uri("http://www.thisisannoying.com/MyEntity");
        request.Properties.Add("MS_HttpConfiguration", config);
        request.Properties.Add("MS_ODataPath", new ODataPath(new EntitySetPathSegment("MyEntity")));
        request.Properties.Add("MS_ODataRouteName", "mynameisbob");

        controller.Request = request;

        var response = controller.Post(new MyEntity());

        Assert.IsNotNull(response);
        Assert.IsTrue(response.IsSuccessStatusCode);
        Assert.AreEqual(HttpStatusCode.Created, response.StatusCode);
    }

I'm not too sure about the IHttpRoute, in the aspnet source code (I had to link to this to figure this all out) the tests use mocks of this interface. So for this test I just create a mock of this and implement the RouteTemplate property and GetVirtualPath method. All the others on the interface were not used during the test.

public class MockRoute : IHttpRoute
{
    public string RouteTemplate
    {
        get { return ""; }
    }

    public IHttpVirtualPathData GetVirtualPath(HttpRequestMessage request, IDictionary<string, object> values)
    {
        return new HttpVirtualPathData(this, "www.thisisannoying.com");
    }

    // implement the other methods but they are not needed for the test above      
}

This is working for me however I am really not too sure about the ODataPath and IHttpRoute and how to set it correctly.

Upvotes: 5

Related Questions