Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature/netcore] How to unit test with ODataQueryOptions #1352

Closed
rmadisonhaynie opened this issue Apr 3, 2018 · 13 comments
Closed

[feature/netcore] How to unit test with ODataQueryOptions #1352

rmadisonhaynie opened this issue Apr 3, 2018 · 13 comments

Comments

@rmadisonhaynie
Copy link

rmadisonhaynie commented Apr 3, 2018

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

Assemblies affected

Microsoft.AspNetCore.OData 7.0.0-beta2

Thank you

@rmadisonhaynie rmadisonhaynie changed the title How to unit test with ODataQueryOptions in OData WebApi lib 7? How to unit test with ODataQueryOptions in Microsoft.AspNetCore.OData 7.0.0-beta2? Apr 3, 2018
@rmadisonhaynie
Copy link
Author

rmadisonhaynie commented Apr 3, 2018

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);

@rmadisonhaynie rmadisonhaynie changed the title How to unit test with ODataQueryOptions in Microsoft.AspNetCore.OData 7.0.0-beta2? [feature/netcore] How to unit test with ODataQueryOptions in Microsoft.AspNetCore.OData 7.0.0-beta2? Apr 4, 2018
@rmadisonhaynie rmadisonhaynie changed the title [feature/netcore] How to unit test with ODataQueryOptions in Microsoft.AspNetCore.OData 7.0.0-beta2? [feature/netcore] How to unit test with ODataQueryOptions Apr 4, 2018
@robward-ms
Copy link
Contributor

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.

@robward-ms
Copy link
Contributor

robward-ms commented Apr 16, 2018

There's a fairly descriptive article about testing AspNetCore controllers here, this might offer some useful suggestions.

@rmadisonhaynie
Copy link
Author

@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.

@robward-ms
Copy link
Contributor

@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.

@rmadisonhaynie
Copy link
Author

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

@freeranger
Copy link
Contributor

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; }
        }
    }
}

@rmadisonhaynie
Copy link
Author

@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!

@AlanWong-MS
Copy link
Contributor

Closing this thread for now. Please comment if new issues arise.

@rmadisonhaynie
Copy link
Author

@AlanWong-MS ok 👍🏻

Also just want to mention @freeranger I used your solution and it’s working great for me, thank you!

@freeranger
Copy link
Contributor

No problem, glad I could help!

@norcino
Copy link

norcino commented Apr 5, 2021

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?

@hakanaltindis
Copy link

@freeranger , I want to hug you :) :) Finally I solved my unit test issue because of you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants