Home > Blockchain >  Stubbing an Asynchronous generic methods
Stubbing an Asynchronous generic methods

Time:12-16

I need to stub an asynchronous generic method in C#. The live method calls a REST API and uses Newtonsoft JSON to deserialise the results into the appropriate format, which will be lists of various types.

Attempting to stub it however runs into various issues. The main code below will fail because it’s not returning a Task.

public Task<T> ExportReport<T>(string reportName)
        {
            if(reportName.Contains("personlookup "))
            {
                List<person> people = new();
                people.Add(new person { Forenames = "Bob", Surname = "Brown" });

                return people;
            }
            else if (reportName.Contains("GetOrder"))
                {
                List<order> orders = new();
                orders.Add(new order { ItemType = “Apples”, ID = "1234" });
                return orders;
            }
            else
            {
                return default;
            }
        }

I have tried various things such as Task.FromResult() and setting the method to async etc and the only thing tried so far that even compiles is

return (Task<T>)people.Cast<T>();

Which fails at runtime with invalid cast exception. Am I doing something wrong here, or is it just impossible for this type of method.

CodePudding user response:

From the comments:

It's not for unit testing. I'm just trying to decouple it from an internal API server so I can work on UI elements offline.

Given it's for that limited purpose then I suppose you could do this:

// Using `typeof(IEnumerable<T>)` instead of `typeof(List<T>)` or `typeof(IList<T>)` because it means `IsAssignableFrom` will work with `List<T>`, `T[]`, `IReadOnlyList<T>`, `IList<T>`, `ImmutableList<T>`, etc.
// It's maddening that even in .NET 6, `IList<T>` still does not extend `IReadOnlyList<T>`. Grumble.

private static readonly _typeofIEnumerablePerson = typeof(IEnumerable<Person>);
private static readonly _typeofIEnumerableOrder  = typeof(IEnumerable<Order>);

public Task<T> ExportReportAsync<T>(string reportName)
{
    T list = GetStubList<T>();
    return Task.FromResult( list );
}

private static TList GetStubList<TList>()
{
    if( _typeofIEnumerablePerson.IsAssignableFrom( typeof(TList) ) )
    {
        List<Person> people = new List<Person>()
        {
            new Person { Forenames = "Bob", Surname = "Brown" }
        };

        return ToTListWorkaround<TList>( people );
    }
    else if( _typeofIEnumerableOrder.IsAssignableFrom( typeof(TList) ) )
    {
        List<Order> orders = new List<Order>()
        {
            new Order { ItemType = "Apples", ID = "1234" }
        };

        return ToTListWorkaround<TList>( orders );
    }
    else
    {
        throw new NotSupportedException( "Unsupported list type: "   typeof(TList).FullName );
    }
}

private static TList ToTListWorkaround<TList>( IEnumerable actual )
{
    // Ugly workaround:
    Object asObject = actual;
    TList returnValue = (TList)asObject;
    return returnValue;
}

CodePudding user response:

Understanding the problem

The problem is not with async generic functions, but rather generic functions in general. You have a generic function that returns an object of type Task<T> where T can be of any type. The type of T is determined by the caller, not the called method. You're trying to stub this function and return a specific type. Imagine the function was declared this way:

public T ExportReport<T>(string reportName)

You would have faced the same problem of how to stub it because of the same reason.

Testing & Refactoring

The fact that you're stubbing implies that you're testing (although you may be not). Sometimes testing reveals design flaws with our code. For example, a generic method that branches over an input argument (a string) to return objects of different types might not be the optimal solution here.

Let's assume that argument "a" returns an object of type A, and argument "b" returns an object of type B. So A myA = ExportReport("a") returns an A, but what happens when you type A myA = ExportReport("b")? Because it's gonna compile and you're gonna have problems somewhere.

Are you really achieving anything by making this function generic?

Maybe you should take this hint from your testing experience and consider a refactor? Perhaps you can have several different non-generic functions that each return a different report type?

Nasty Solution (.NET5.0 or greater)

If you don't care about any of this and just want to get it to work, you can do something like this:

public T ExportReport<T>(string reportName)
{
    if (reportName.Contains("personlookup "))
    {
        List<Person> people = new();
        people.Add(new Person { Forenames = "Bob", Surname = "Brown" });

        return Unsafe.As<List<Person>,T>(ref people);
    }

    return default;
}

And the `Task version:

public Task<T> ExportReport<T>(string reportName)
{
    if (reportName.Contains("personlookup "))
    {
        List<Person> people = new();
        people.Add(new Person { Forenames = "Bob", Surname = "Brown" });
        var peopleTask = Task.FromResult(people);
        return Unsafe.As<Task<List<Person>>,Task<T>>(ref peopleTask);
    }

    throw new ArgumentOutOfRangeException(nameof(reportName));
}

Better Solution

Sounds like you can serialize your stub-data and desterilize it using your regular JSON converter, which also solves the problem.

Best Solution

Probably refactor.

  • Related