具有类型变量的模拟通用方法 - NSubstitute

发布于 2025-01-16 13:36:41 字数 3238 浏览 0 评论 0 原文

我有一个如下所示的客户端界面:

public interface IDiscosClient
{
    public Task<DiscosResponse<T>?> Get<T>(string queryUrl) where T : DiscosModelBase;
    // The rest
}

DiscosResponse 如下所示:

public record DiscosResponse<T> where T: DiscosModelBase
{
    public DiscosResponse()
    {
        
    }

    internal DiscosResponse(T attributes)
    {
        Attributes = attributes;
    }

    [JsonPropertyName("type")]
    public ResponseType Type { get; init; }
    
    // TODO - This is wrong an probably needs renaming to something like Object
    [JsonPropertyName("attributes")]
    public T Attributes { get; init; }
    
    [JsonPropertyName("id")]
    [JsonConverter(typeof(JsonStringIntConverter))]
    public int Id { get; init; }
}

我希望能够动态创建 Substitute.For()<此接口的 /code> 将始终构建并返回 T 的实例。

所以我知道如何构造和调用泛型方法。我还设置了 AutoFixture,以便我可以根据需要创建 T 的新实例。

然而,我不知道的是如何告诉 NSubstitute 在调用这个构造方法时返回这个新实例。

作为参考,在没有反射的情况下执行此操作的常用语法是:

MyType myMock = Substitute.For<MyType>();
myMock.MyMethod().Returns(myInstance);

编辑:

我必须在其中的自动修复部分中放置一个引脚,因为它会导致递归问题。但是,我现在想出了这个,它似乎一直有效,直到我尝试设置调用的返回值:

private IDiscosClient CreateSubstituteClient(Type type)
    {
        IDiscosClient client = Substitute.For<IDiscosClient>();
        MethodInfo getMethod = typeof(IDiscosClient).GetMethod(nameof(client.Get), new [] {typeof(string)}) ?? throw new MethodAccessException();
        MethodInfo constructedGetMethod = getMethod.MakeGenericMethod(type);

        Type constructedReturnType = typeof(DiscosResponse<>).MakeGenericType(type);
        const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
        CultureInfo culture = CultureInfo.InvariantCulture;
        object returnValue = Activator.CreateInstance(constructedReturnType, flags, null, new [] {Activator.CreateInstance(type)}, culture); // Not using AutoFix as it will cause recursion issues

        constructedGetMethod.Invoke(client, new object?[] {Arg.Any<string>()}).Returns(Task.FromResult(returnValue));
        return client;
    }

此时它会抛出此错误:

NSubstitute.Exceptions.CouldNotSetReturnDueToTypeMismatchException: 无法为 IDiscosClient.Get 返回类型为 Task1 的值(预期类型为 Task1)。

这很令人困惑,因为 returnValue 的类型是:

DISCOSweb_Sdk.Models.DiscosResponse`1[[DISCOSweb_Sdk.Models.ResponseModels.Reentries.Reentry, DISCOSweb-Sdk,版本=1.0.0.0,文化=中性, PublicKeyToken=null]],DISCOSweb-Sdk,版本=1.0.0.0, 文化=中立,PublicKeyToken=null

并且 constructedGetMethod.ReturnParameter 是:

System.Threading.Tasks.Task1[DISCOSweb_Sdk.Models.DiscosResponse1[DISCOSweb_Sdk.Models.ResponseModels.Reentries.Reentry]]

其中,一旦我将前者包装在 Task 中,AFIACT 就会匹配。从结果

I have a client interface that looks like this:

public interface IDiscosClient
{
    public Task<DiscosResponse<T>?> Get<T>(string queryUrl) where T : DiscosModelBase;
    // The rest
}

And DiscosResponse<T> looks like this:

public record DiscosResponse<T> where T: DiscosModelBase
{
    public DiscosResponse()
    {
        
    }

    internal DiscosResponse(T attributes)
    {
        Attributes = attributes;
    }

    [JsonPropertyName("type")]
    public ResponseType Type { get; init; }
    
    // TODO - This is wrong an probably needs renaming to something like Object
    [JsonPropertyName("attributes")]
    public T Attributes { get; init; }
    
    [JsonPropertyName("id")]
    [JsonConverter(typeof(JsonStringIntConverter))]
    public int Id { get; init; }
}

I want to be able to be able to dynamically create a Substitute.For<T>() of this interface that will always build and return an instance of T.

So I know how to construct and call a generic method. I also have AutoFixture set up so that I can create new instances of T on demand.

What I don't know, however, is how to then go about telling NSubstitute to return this new instance when this constructed method is called.

For reference, the usual syntax for doing this without reflection would be:

MyType myMock = Substitute.For<MyType>();
myMock.MyMethod().Returns(myInstance);

Edit:

I've had to put a pin in the AutoFix part of this because it was causing recursion issues. However, I've now come up with this, which seems to work right up until I try and set the return value on the invocation:

private IDiscosClient CreateSubstituteClient(Type type)
    {
        IDiscosClient client = Substitute.For<IDiscosClient>();
        MethodInfo getMethod = typeof(IDiscosClient).GetMethod(nameof(client.Get), new [] {typeof(string)}) ?? throw new MethodAccessException();
        MethodInfo constructedGetMethod = getMethod.MakeGenericMethod(type);

        Type constructedReturnType = typeof(DiscosResponse<>).MakeGenericType(type);
        const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
        CultureInfo culture = CultureInfo.InvariantCulture;
        object returnValue = Activator.CreateInstance(constructedReturnType, flags, null, new [] {Activator.CreateInstance(type)}, culture); // Not using AutoFix as it will cause recursion issues

        constructedGetMethod.Invoke(client, new object?[] {Arg.Any<string>()}).Returns(Task.FromResult(returnValue));
        return client;
    }

At which point it throws this error:

NSubstitute.Exceptions.CouldNotSetReturnDueToTypeMismatchException:
Can not return value of type Task1 for IDiscosClient.Get (expected type Task1).

Which is confusing because the type of returnValue is:

DISCOSweb_Sdk.Models.DiscosResponse`1[[DISCOSweb_Sdk.Models.ResponseModels.Reentries.Reentry,
DISCOSweb-Sdk, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=null]], DISCOSweb-Sdk, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null

And constructedGetMethod.ReturnParameter is:

System.Threading.Tasks.Task1[DISCOSweb_Sdk.Models.DiscosResponse1[DISCOSweb_Sdk.Models.ResponseModels.Reentries.Reentry]]

Which, AFIACT match once I wrap the former in a Task.FromResult

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

栖竹 2025-01-23 13:36:41

Task.FromResult(returnValue) 结果是 Task的运行时类型,而您的方法需要 Task?>NSubstitute 检查返回类型与(以及其他)IsAssignableFrom 的兼容性,因此它会抛出异常。在这种特殊情况下,您需要这样做

var methodInfo = typeof(Task).GetMethod(nameof(Task.FromResult), BindingFlags.Static | BindingFlags.Public);
var fromResult = methodInfo.MakeGenericMethod(constructedReturnType).Invoke(null, new []{ returnValue});

constructedGetMethod.Invoke(client, new object?[] {Arg.Any<string>()}).Returns(fromResult);

才能使运行时类型相同。

Task.FromResult(returnValue) results in runtime type of Task<object> while your method expects Task<DiscosResponse<T>?>. NSubstitute checks compatibility of returned type with(among others) IsAssignableFrom so it throws exception. In this particular case you need to do sth like this

var methodInfo = typeof(Task).GetMethod(nameof(Task.FromResult), BindingFlags.Static | BindingFlags.Public);
var fromResult = methodInfo.MakeGenericMethod(constructedReturnType).Invoke(null, new []{ returnValue});

constructedGetMethod.Invoke(client, new object?[] {Arg.Any<string>()}).Returns(fromResult);

in order for runtime types to be the same.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文