Moq 返回异步在元组<对象,布尔值> 时返回 Null 而不是提供的值



这是我到目前为止的第一个问题,如果描述得不是很好,请原谅我,但我会尽力而为。

我正在使用 Moq 在单元测试中模拟我的服务层,以进行后 api 调用和_service。创建(...) 返回一个元组值:
Task<(Model.Receipt Receipt, bool IsIdempotent)>因此,我创建了一个元组结果并作为 ReturnsAsync 传递,如下所示:

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

到目前为止一切正常,但在运行时,在 Post 调用中调用服务后,返回值为<null,false>!!!
这听起来像是返回默认值而不是预期的元组。由于我在这次模拟后有日志记录数据,这会导致单元测试失败。
你知道我在这里是否遗漏了什么吗?

此设置

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

..说如果调用CreateAsync并且参数input,则返回output。这仅在参数实际上是相同的对象,与input相同的类实例时才有效。

有时这有效,但通常无效。在这种情况下,input从字符串反序列化。如果您要测试的代码也反序列化字符串,则最终会得到两个Receipt实例。它们可能是相同的,但它们不是同一个实例,因此Setup无法按您想要的方式工作。

您可能想要的是设置模拟,以便如果您调用CreateAsync并且input具有某些属性值,则模拟返回output

我不知道Receipt长什么样子。为了演示,假设它看起来像这样

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

。并且您希望您的Setup返回output如果传递给CreateAsync的参数与input具有相同的IdName。在这种情况下,您可以这样做:

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

这表示Setup正在寻找一个Receipt参数,当它得到该参数时,它将执行此函数,该函数返回true如果参数的IdName属性与inputIdName匹配

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

如果您打算编写大量此类测试,并且不想一遍又一遍地编写该函数怎么办?您还可以创建如下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);
}
}

除非你在生产代码中需要这个,否则我会在测试项目中定义这个类。此类包含用于比较两个Receipt实例并确定它们是否相等的逻辑。

现在,您的Setup可能如下所示:

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

现在,Setup将传递给CreateAsync的参数,并使用ReceiptEqualityComparer来确定该参数是否"等于"input。如果他们具有相同的Id并且Name它们是平等的。


最后,如果Receipt实现IEquatable<Receipt>,您在问题中发布的原始代码将起作用。这意味着该类具有自己的内置逻辑,用于比较属性以查看两个实例是否相等。我使用ReSharper为我自动生成它。它看起来像这样:

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

这也许是有道理的。但是,如果您只需要将其用于测试,那么我更喜欢前两种方法中的任何一种,而不是修改生产类以使测试正常工作。

最新更新