简体   繁体   English

Moq ReturnsAsync 在元组时返回 Null<object,bool> 而不是提供的价值</object,bool>

[英]Moq ReturnsAsync returns Null when tuple<object,bool> instead of provided value

This is my first question so far, excuse me if it is not very well-described but I will try my best.到目前为止,这是我的第一个问题,如果描述得不是很好,请原谅,但我会尽力而为。

I am using Moq to mock my service layer in unit test for a post api call, and the _service.Create(...) returns a tuple value: Task<(Model.Receipt Receipt, bool IsIdempotent)>我正在使用 Moq 在 api 后调用的单元测试中模拟我的服务层,并且 _service.Create(...) 返回一个元组值: Task<(Model.Receipt Receipt, bool IsIdempotent)>
therefor I created a tuple result and pass as ReturnsAsync like below:为此,我创建了一个元组结果并作为 ReturnsAsync 传递,如下所示:

var input = JsonConvert.DeserializeObject<Model.Receipt>(_jsonReceiptString);
var output = (Receipt: input, IsIdempotent: true);
_service.Setup(x => x.CreateAsync(input)).ReturnsAsync(output);

everything works fine till here, but at run time, after calling the service in the Post call, the return value is <null,false> !!!到目前为止一切正常,但在运行时,在 Post 调用中调用服务后,返回值为<null,false> !!!
It is sound like returning a default value instead of expected tuple.这听起来像是返回默认值而不是预期的元组。 As I have logging data after this mocking, this causes a failed unit test.由于我在此 mocking 之后记录了数据,这会导致单元测试失败。
Do you have any idea if I am missing something here?如果我在这里遗漏了什么,你知道吗?

This setup这个设置

_service.Setup(x => x.CreateAsync(input)).ReturnsAsync(output);

..says that if CreateAsync is called and the argument is input , return output . ..说如果CreateAsync并且参数是input ,则返回output This only works if the argument is actually the same object, the same class instance as input .这仅在参数实际上是相同的 object、相同的 class 实例作为input时才有效。

Sometimes that works, but often it doesn't.有时这有效,但通常无效。 In this case input is deserialized from a string.在这种情况下, input是从字符串反序列化的。 If the code you're testing also deserializes a string you'll end up with two instances of Receipt .如果您正在测试的代码还反序列化一个字符串,您最终会得到两个Receipt实例。 They might be identical, but they're not the same instance so the Setup doesn't work the way you want.它们可能相同,但它们不是同一个实例,因此Setup无法按您希望的方式工作。

What you likely want is to set up the mock so that if you call CreateAsync and input has certain property values then the mock returns output .您可能想要设置模拟,以便如果您调用CreateAsync并且input具有某些属性值,则模拟返回output

I don't know what Receipt looks like.我不知道Receipt是什么样的。 For the sake of demonstration let's say it looks like this为了演示,假设它看起来像这样

internal class Receipt
{
    public int Id { get; set; }
    public string Name { get; set; }
}

...and you want your Setup to return output if the argument passed to CreateAsync has the same Id and Name as input . ...如果传递给CreateAsync的参数与input具有相同的IdName ,您希望您的Setup返回output In that case you could do this:在这种情况下,您可以这样做:

_service.Setup(x =>
        x.CreateAsync(
            It.Is<Models.Receipt>(receipt =>
                receipt.Id == input.Id
                && receipt.Name == input.Name)))
    .ReturnsAsync(output);

This says that Setup is looking for a Receipt argument, and when it gets that argument it's going to execute this function which returns true if the argument's Id and Name property match input 's Id and Name :这表示Setup正在寻找一个Receipt参数,当它获取该参数时,它将执行此 function 如果参数的IdName属性匹配inputIdName ,则返回true

receipt =>
    receipt.Id == input.Id
    && receipt.Name == input.Name

What if you plan on writing a lot of these tests and you don't want to write that function over and over?如果您打算编写大量此类测试并且不想一遍又一遍地编写 function 怎么办? You can also create an IEqualityComparer like this:您还可以像这样创建一个IEqualityComparer

public class ReceiptEqualityComparer : IEqualityComparer<Receipt>
{
    public bool Equals(Receipt x, Receipt y)
    {
        return x.Id == y.Id && x.Name == y.Name;
    }

    public int GetHashCode(Receipt obj)
    {
        return HashCode.Combine(obj.Id, obj.Name);
    }
}

Unless you need this in your production code I would define this class in the test project.除非您在生产代码中需要这个,否则我会在测试项目中定义这个 class。 This class contains the logic to compare two instances of Receipt and decide if they are equal.此 class 包含比较两个Receipt实例并确定它们是否相等的逻辑。

Now your Setup can look like this:现在您的Setup可能如下所示:

_service.Setup(x =>
        x.CreateAsync(
            It.Is<Models.Receipt>(input, new ReceiptEqualityComparer())))
    .ReturnsAsync(output);

Now Setup will take the argument passed to CreateAsync and use ReceiptEqualityComparer to determine whether that argument is "equal" to input .现在, Setup将获取传递给CreateAsync的参数并使用ReceiptEqualityComparer来确定该参数是否“等于” input If they have the same Id and Name they are equal.如果它们具有相同的IdName ,它们是相等的。


Finally, your original code as posted in the question will work if Receipt implements IEquatable<Receipt> .最后,如果Receipt实现IEquatable<Receipt> ,则问题中发布的原始代码将起作用。 That means that the class has its own built-in logic for comparing properties to see if two instances are equal.这意味着 class 具有自己的内置逻辑,用于比较属性以查看两个实例是否相等。 I use ReSharper to auto-generate that for me.我使用 ReSharper 为我自动生成它。 It looks like this:它看起来像这样:

public class Receipt : IEquatable<Receipt>
{
    public int Id { get; set; }
    public string Name { get; set; }

    public bool Equals(Receipt? other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return Id == other.Id && Name == other.Name;
    }

    public override bool Equals(object? obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((Receipt) obj);
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Id, Name);
    }
}

That might make sense.这可能是有道理的。 But if you only need this for your test then I'd prefer either of the previous two approaches instead of modifying the production class so that the test works.但是,如果您只需要它来进行测试,那么我更喜欢前两种方法中的任何一种,而不是修改生产 class 以便测试有效。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM