这是我到目前为止的第一个问题,如果描述得不是很好,请原谅我,但我会尽力而为。
我正在使用 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
具有相同的Id
和Name
。在这种情况下,您可以这样做:
_service.Setup(x =>
x.CreateAsync(
It.Is<Models.Receipt>(receipt =>
receipt.Id == input.Id
&& receipt.Name == input.Name)))
.ReturnsAsync(output);
这表示Setup
正在寻找一个Receipt
参数,当它得到该参数时,它将执行此函数,该函数返回true
如果参数的Id
和Name
属性与input
的Id
和Name
匹配
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);
}
}
这也许是有道理的。但是,如果您只需要将其用于测试,那么我更喜欢前两种方法中的任何一种,而不是修改生产类以使测试正常工作。