Avanade / UnitTestEx

UnitTestEx provides .NET testing extensions to the most popular testing frameworks (MSTest, NUnit and Xunit) specifically to improve the testing experience with ASP.NET controller, and Azure Function, execution including underlying HttpClientFactory mocking.
MIT License
18 stars 4 forks source link

HttpTriggerTester Expression Tree Limitations #69

Open m-flak opened 2 months ago

m-flak commented 2 months ago

HttpTriggerTester Expression Tree Limitations

Background

In my tests project, I have created an extension method using reflection to automatically get the route of my HTTP-Triggered-Functions with that method. The goal here was to eliminate requirements for test code updates after changes to a Function's route in the main code. It almost works.

I've since written my code around this limitation, but it requires me to spin up & run an entire HostTesterBase implementor twice. 😰

Issue

HttpTriggerTester's Run/RunAsync methods, unlike TypeTester's, uses expression trees. SRC

public async Task<ActionResultAssertor> RunAsync(Expression<Func<TFunction, Task<IActionResult>>> expression)

This means that I am unable to write something like this without experiencing compilation errors:

        var sut = await test
            .HttpTrigger<QueueTaskCreateFunction>()
            .RunAsync(f => f.Run(test.CreateHttpRequest(HttpMethod.Post, f.GetFunctionRoute(), contentType: MediaTypeNames.Text.Plain), context.Object));

A statement like this would work with TypeTester, but I can't use TypeTester to test my HTTP-Triggered-Function.

Is there a technical reason for HttpTriggerTester's Run/RunAsync methods not having an overload for Action<T> or Func<T1,T2>??

Workaround

The following boilerplate accomplishes what I intended without any issues:

       var route = test
            .Type<QueueTaskCreateFunction>()
            .Run(f => f.GetFunctionRoute())
            .Result;

        var request = test
            .CreateHttpRequest(HttpMethod.Post, route, contentType: MediaTypeNames.Text.Plain);

        var sut = await test
            .HttpTrigger<QueueTaskCreateFunction>()
            .RunAsync(f => f.Run(request, context.Object));
chullybun commented 1 month ago

Hi @m-flak,

The expression usage is by design as this provides an opportunity for UnitTestEx to verify/assert that the method being invoked as HTTP, i.e. has the HttpTriggerAttribute. And, that the corresponding HTTP method is expected.

A backlog item is for this to further inspect the Route and verify what is passed matches accordingly. Today, this is accepted as-is and technically anything can be passed in as the route content.

I am not sure that a test should infer a value in the code being tested and then be used in the actual test. The test should assert based on intent; otherwise, how do you know whether there is an issue, e.g. maybe a misspelling in the route?

How often do the routes change that this is an issue?

Thanks...

m-flak commented 1 month ago

I am not sure that a test should infer a value in the code being tested and then be used in the actual test. The test should assert based on intent; otherwise, how do you know whether there is an issue, e.g. maybe a misspelling in the route?

How often do the routes change that this is an issue?

Okay, all I wanted to do was get the route value automatically from the attribute and use it with CreateHttpRequest. There's no way to do that currently, so I attempted to implement that on my end.

A backlog item is for this to further inspect the Route [...]

This is the gap I tried to fill that led me to this issue. I wanted the ability to use the metadata within HttpTriggerAttribute to automatically create test http requests with the correct route.

m-flak commented 1 month ago

I rethought my approach and made a simple utility method. Still, it would be cool if UnitTestEx could do this for me.

    public static string GetHttpRoute(Type functionClass)
    {
        string? route = null;

        var runMethod = functionClass.GetMethod("Run");
        var runParams = runMethod?.GetParameters();
        if (runMethod is null
            || runParams is null
            || runParams.Length == 0)
        {
            return string.Empty;
        }

        var attribute = runParams[0]
            .GetCustomAttributes(true)
            .FirstOrDefault(
                a => typeof(HttpTriggerAttribute).IsInstanceOfType(a),
                null)
            as HttpTriggerAttribute;

        route = attribute?.Route;
        route ??= string.Empty;
        return route;
    }