Hello, I'm wondering if someone could help me with how to unit test my controller methods that take ODataQueryOptions as a param.
My simpe controller:
[ODataRoute()]
public IQueryable<T> Get(ODataQueryOptions opts)
{
VerifySelectExpandOptions(opts);
return _dbContext.Set<T>().AsQueryable();
}
I can find many examples of how to create an ODataQueryOptions object in OData WebApi 6.x but can't get it to work in Microsoft.AspNetCore.OData 7.0.0-beta2
Microsoft.AspNetCore.OData 7.0.0-beta2
Thank you
In fact I see that you have a RequestFactory class to Create your HttpRequest object that ends up being quite nightmarish and uses internal methods that I don't have access to!
Example:
var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel();
var message = RequestFactory.Create(
HttpMethod.Get,
"http://server/service/Customers/?$filter=Filter&$select=Select&$orderby=OrderBy&$expand=Expand&$top=10&$skip=20&$count=true&$skiptoken=SkipToken&$deltatoken=DeltaToken"
);
var queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), message);
RequestFactory exists so the UTs only need to create a request using the same code even though it's quite different under the hood.
You can create a new DefaultHttpContext() and access the .Request property to create an HttpRequest in UTs.
There's a fairly descriptive article about testing AspNetCore controllers here, this might offer some useful suggestions.
@robward-ms thank you for the suggestions but I have tried using DefaultHttpContext() to get an HttpRequest but the problem is when creating the ODataQueryOptions object there is a lot of validation/requirements on the configuration of the HttpRequest object. For example eventually from ODataQueryOptions constructor I end up here:
private static IServiceScope CreateRequestScope(this HttpRequest request, string routeName)
{
IPerRouteContainer perRouteContainer = request.HttpContext.RequestServices.GetRequiredService<IPerRouteContainer>();
if (perRouteContainer == null)
{
throw Error.InvalidOperation(SRResources.MissingODataServices, nameof(IPerRouteContainer));
}
IServiceProvider rootContainer = perRouteContainer.GetODataRootContainer(routeName);
IServiceScope scope = rootContainer.GetRequiredService<IServiceScopeFactory>().CreateScope();
// Bind scoping request into the OData container.
if (!string.IsNullOrEmpty(routeName))
{
scope.ServiceProvider.GetRequiredService<HttpRequestScope>().HttpRequest = request;
}
return scope;
}
So if I don't have RequestServices with IPerRouteContainer registered I get an exception, then there's more and more.
I started also trying to use wrappers for ODataQueryOptions so I could create mocks but this ended up being too much work as well as I had to create a wrapper for SelectExpandQueryOption, then SelectExpandClause, then SelectItem, etc etc etc.
Sorry for mentioning the controller, forget about that :). I'm really trying to unit test my own custom query validation methods that take ODataQueryOptions as it's only param. I imagine I could eventually get something to work, but I was just hoping there may be a easier path.
@rmadisonhaynie - Ya, we have the same problem in our UTs. You could "borrow" the Request and Config factory classes form the UT abstraction project.
As an alternative, we could look at creating a simply way to construct a ODataQueryOptions without a request but I think that is a UT-only scenario.
I'll try to borrow your UT factory classes for now but yes I'd imagine for future .netcore OData users an easier way to unit test with ODataQueryOptions would be appreciated
This may or may not be useful to you.
Our use case was an action filter to validation the options, so to unit test we needed an ActionExecutingContext which had an ODataQueryOptions action argument.
This is the "meat" of the test class which tested passing a $top=1 query.
using System;
using System.Collections.Generic;
using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Builder;
using Microsoft.AspNet.OData.Extensions;
using Microsoft.AspNet.OData.Query;
using Microsoft.AspNet.OData.Query.Validators;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OData.UriParser;
using Moq;
using NUnit.Framework;
using HttpRequest = Microsoft.AspNetCore.Http.HttpRequest;
namespace OData.Tests
{
[TestFixture]
[Category("Unit")]
public class ValidateODataQueryOptionsFilterAttributeTests
{
private HttpContext _httpContext;
private ServiceProvider _provider;
private ODataValidationSettings _settings;
[OneTimeSetUp]
public void BeforeAllTests()
{
_settings = new ODataValidationSettings
{
AllowedQueryOptions = AllowedQueryOptions.Top
};
var collection = new ServiceCollection();
collection.AddOData();
collection.AddODataQueryFilter();
collection.AddTransient<ODataUriResolver>();
collection.AddTransient<ODataQueryValidator>();
collection.AddTransient<TopQueryValidator>();
_provider = collection.BuildServiceProvider();
var routeBuilder = new RouteBuilder(Mock.Of<IApplicationBuilder>(x => x.ApplicationServices == _provider));
routeBuilder.EnableDependencyInjection();
}
[Test]
public void Some_Unit_Test()
{
// Arrange
var context = GetActionContextFor("$top=1");
var target = new OurCustomActionFilterAttribute();
// Act
target.OnActionExecuting(context);
// Assert
// Assert some stuff about it.
}
private ActionExecutingContext GetActionContextFor(string url)
{
var uri = new Uri($"http://localhost/api/mytype/12345?{url}");
_httpContext = new DefaultHttpContext
{
RequestServices = _provider
};
// ReSharper disable once UnusedVariable
HttpRequest request = new DefaultHttpRequest(_httpContext)
{
Method = "GET",
Host = new HostString(uri.Host, uri.Port),
Path = uri.LocalPath,
QueryString = new QueryString(uri.Query)
};
// ReSharper disable once UnusedVariable
HttpResponse response = new DefaultHttpResponse(_httpContext)
{
StatusCode = StatusCodes.Status200OK
};
var modelBuilder = new ODataConventionModelBuilder(_provider);
var entitySet = modelBuilder.EntitySet<TestType>("TestType");
entitySet.EntityType.HasKey(entity => entity.SomeProperty);
var model = modelBuilder.GetEdmModel();
var actionArguments = new Dictionary<string, object>();
var actionContext = new ActionExecutingContext(new ActionContext(_httpContext, new RouteData(), new ActionDescriptor(), new ModelStateDictionary()), new List<IFilterMetadata>(), actionArguments, null);
var context = new ODataQueryContext(model, typeof(TestType), new Microsoft.AspNet.OData.Routing.ODataPath());
var options = new ODataQueryOptions<TestType>(context, actionContext.HttpContext.Request);
actionArguments.Add("options", options);
return actionContext;
}
internal class TestType
{
public string SomeProperty { get; set; }
}
}
}
@freeranger yikes that's a lot of setup :). My setup up is a little different but I think I can adapt your example to get something to work for myself. In fact I didn't realize you could use ODataQueryOptions in an ActionFilter which is actually probably what I'll refactor to doing, so this is perfect thank you!
Closing this thread for now. Please comment if new issues arise.
@AlanWong-MS ok 馃憤馃徎
Also just want to mention @freeranger I used your solution and it鈥檚 working great for me, thank you!
No problem, glad I could help!
I know it is old, I am trying to use the solution above but I cannot satisfy "using Microsoft.AspNetCore.Http.Internal;" with net core 3.1, I remember I used something similar for .net core 2.1.
Can anyone give me a steer to get this sorted?
Most helpful comment
This may or may not be useful to you.
Our use case was an action filter to validation the options, so to unit test we needed an ActionExecutingContext which had an ODataQueryOptions action argument.
This is the "meat" of the test class which tested passing a $top=1 query.