Xunit 中的模拟 - 如何验证 returns 某个值的方法
Mocking in Xunit- How to verify method that returns some value
我创建了一个验证方法被调用的单元测试方法,下面是 same.The 方法构建电子邮件对象并调用 GeneratePDF 方法的代码,该方法进一步 returns 字节 BuildEmailInfo 方法 returns 电子邮件对象。
public class SMTPEmailSender : IEmailSender
{
private IPDFCreater _pdfCreater;
public SMTPEmailSender(IPDFCreater pdfCreater)
{
_pdfCreater = pdfCreater;
}
public Email BuildEmailInfo(string sMTPServerUrl, FaxMailDTO faxAsMailRequest)
{
Email email=null;
try
{
var otp = new PDFData { OTP =faxAsMailRequest.OTP};
email = new Email
{
SMTPServerUrl = sMTPServerUrl,
Encoding = Encoding.UTF8,
ToAddress = faxAsMailRequest.ToEmailAddress,
ToAddressDisplayName = faxAsMailRequest.ToAddressDisplayName,
FromAddress = faxAsMailRequest.FromEmailAddress,
Subject = faxAsMailRequest.Subject,
Body = faxAsMailRequest.Body,
FromAddressDisplayName = faxAsMailRequest.FromAddressDisplayName,
ContentStream = new MemoryStream(_pdfCreater.GeneratePDF(otp)),
AttachmentName = faxAsMailRequest.FaxFileName
};
}
catch(Exception ex)
{
Log.Error("Method : BuildEmailInfo. Exception raised while building email data : {@Message}", ex.Message, ex);
}
return email;
}
下面是我的单元测试代码,每当我执行它时,它都会抛出一个错误 Expected invocation on the mock at least once, but was never performed: x=>x.GeneratePDF(pdfdata)。另外让我知道执行测试的方法是否正确
public class SMTPEmailSenderTest
{
private SMTPEmailSender _sMTPEmailSender;
Mock<IPDFCreater> _mockPdfCreator;
public SMTPEmailSenderTest()
{
_mockPdfCreator = new Mock<IPDFCreater>();
_sMTPEmailSender = new SMTPEmailSender(_mockPdfCreator.Object);
}
[Theory]
[MemberData(nameof(GetFaxAsMailObject))]
public void BuildEmailInfoTest_ReturnsValidEmailObject(FaxMailDTO faxMailDTO)
{
string smpturl = "localhost";
var otp = new PDFData { OTP = faxMailDTO.OTP };
var result = _sMTPEmailSender.BuildEmailInfo(smpturl, faxMailDTO);
_mockPdfCreator.Verify(x => x.GeneratePDF(otp));
}
}
模拟对象的验证应该是您在单元测试中的'last resort'。想一想:如果 PDF 创建者不调用 GeneratePDF 方法,是否违反了实际要求?用户只关心PDF生成了。
在这种情况下,您可以直接验证BuildEmailInfo方法的结果,例如:
var result = _sMTPEmailSender.BuildEmailInfo(smpturl, faxMailDTO);
var expectedBytes = ...; // TODO - get the expected byte[] array from somewhere
Assert.Equal(expectedBytes, result.ContentStream.ToArray());
此外,您也许可以在完全不模拟依赖项的情况下编写此测试?如果可以调用实际的 PDF creator 对象在内存中生成 byte[] 数组,则可以使用真实对象而不是模拟它。
这一行:
_mockPdfCreator.Verify(x => x.GeneratePDF(otp));
执行 'verification'。这是一个断言,用于检查方法 .GeneratePDF
是否已在 _mockPdfCreator
上以 otp
作为参数调用。
Mock 对象接口中的所有 .Verify 方法都用于检查是否调用了某些方法或 属性。你还可以提供一些过滤器来查看是否传递了某些参数,例如:
_myMock.Verify(x => x.FooBar(5));
_myMock.Verify(x => x.FooBar(123));
_myMock.Verify(x => x.FooBar(It.IsAny<int>());
_myMock.Verify(x => x.FooBar(It.Is<int>(number => (number-5)%3 > 10));
所有这些都检查 FooBar
是否在 _myMock
上,但它们每个都只查看使用特定参数值的调用:5、123、anything-that-is-int 或(...).
您不能使用 .Verify 检查 return 值。
那里没有这样的选项。
为什么?想想看。你有:
_mockPdfCreator = new Mock<IPDFCreater>();
....
_mockPdfCreator.Verify(x => x.GeneratePDF(otp));
_mockPdfCreator
是您的模拟对象。不是真的。这是一个小幽灵,就像是某个 IPDFCreater 一样。
那里没有丝毫真正的实现。
你怎么能指望 GeneratePDF
return 有意义?
它只是不会。后面什么也没有。如果有任何东西调用该方法 GeneratePDF
,它将 return NULL(或抛出异常,具体取决于模拟模式:Loose/Strict)。
...除非你设置你的模拟来做不同的事情:
var theThing = ...;
_mockPdfCreator = new Mock<IPDFCreater>();
_mockPdfCreator.Setup(x => x.GeneratePDF(It.IsAny<...>())).Returns(theThing);
....
// ... now check what `GeneratePDF` returned?!
现在任何调用GeneratePDF
方法的,都会returntheThing
。好的。
但是你已经知道了。没有什么可检查的。您将 GeneratePDF 设置为 return 的东西,因此没有丝毫检查 GeneratePDF return 的意义。这是您的模拟和设置!
Sooo,如果有任何名为 GeneratePDF 的东西,那么 NULL 将被 returned,因为没有 GeneratePDF 的设置。但是,正如 Verify 所证明的那样,从未调用过 GeneratePDF。这意味着当您创建 SMTPEmailSender 时,将模拟作为参数提供给它:
_mockPdfCreator = new Mock<IPDFCreater>();
_sMTPEmailSender = new SMTPEmailSender(_mockPdfCreator.Object);
然后在测试中你有:
....
var result = _sMTPEmailSender.BuildEmailInfo(smpturl, faxMailDTO);
_mockPdfCreator.Verify(x => x.GeneratePDF(otp));
然后,显然 _sMTPEmailSender.BuildEmailInfo
根本不想调用 GeneratePDF
。
为什么?不知道。很可能 smpturl
或 faxMailDTO
中的某些内容对于此用例被认为是无效的,并且跳过了 generate-pdf 步骤。检查结果。查看是否有任何错误或消息可以告诉您为什么它甚至没有尝试调用 GeneratePDF。
另请注意,您所写的验证是
x => x.GeneratePDF(otp)
这很具体。它有 hard-coded 对 otp
的引用。所以也许它被调用了,但是参数值不同?
尝试添加:
var result = _sMTPEmailSender.BuildEmailInfo(smpturl, faxMailDTO);
_mockPdfCreator.Verify(x => x.GeneratePDF(It.IsAny<PDFData>())); // <-
_mockPdfCreator.Verify(x => x.GeneratePDF(otp));
或类似的东西,看看哪个验证失败。如果前者通过而后者失败,那么一切都很好,只是不是您期望的确切 OTP(也许 _sMTPEmailSender 克隆了它?等等)。
万一前者失败了,那就意味着 GeneratePDF
真的连一次都没有被调用,然后这意味着你必须了解为什么 BuildEmailInfo
带有参数(smpturl, faxMailDTO) 没有达到您的预期。你那里有一个 try-catch-log。也许一些空引用表达式?但我对此表示怀疑。
你已经做到了:
[MemberData(nameof(GetFaxAsMailObject))] /// <==== B
public void BuildEmailInfoTest_ReturnsValidEmailObject(FaxMailDTO faxMailDTO) // <--- A
{
...
var otp = new PDFData { OTP = faxMailDTO.OTP }; //<--- C
...
_mockPdfCreator.Verify(x => x.GeneratePDF(otp)); //<---D
因此,faxMailDTO
来自 GetFaxAsMailObject。 BuildEmailInfo 通过参数获取它并将它的一部分传递给 GeneratePDF。然后你在 Verify that D uses newly-constructed otp
中断言来自 C 行。那是行不通的。来自 A+B 的 faxMailDTO
所以来自 GetFaxAsMailObject
当然不包含来自 C 的 otp
并且当然不会将 otp
对象传递给 GeneratePDF。 GeneratePDF 将从 faxMailDTO
来自 A+B.
的其他一些 PDFData 对象
我想我已经说得够多了,涵盖了您的测试设置的所有问题。您几乎是对的。祝你好运!
我创建了一个验证方法被调用的单元测试方法,下面是 same.The 方法构建电子邮件对象并调用 GeneratePDF 方法的代码,该方法进一步 returns 字节 BuildEmailInfo 方法 returns 电子邮件对象。
public class SMTPEmailSender : IEmailSender
{
private IPDFCreater _pdfCreater;
public SMTPEmailSender(IPDFCreater pdfCreater)
{
_pdfCreater = pdfCreater;
}
public Email BuildEmailInfo(string sMTPServerUrl, FaxMailDTO faxAsMailRequest)
{
Email email=null;
try
{
var otp = new PDFData { OTP =faxAsMailRequest.OTP};
email = new Email
{
SMTPServerUrl = sMTPServerUrl,
Encoding = Encoding.UTF8,
ToAddress = faxAsMailRequest.ToEmailAddress,
ToAddressDisplayName = faxAsMailRequest.ToAddressDisplayName,
FromAddress = faxAsMailRequest.FromEmailAddress,
Subject = faxAsMailRequest.Subject,
Body = faxAsMailRequest.Body,
FromAddressDisplayName = faxAsMailRequest.FromAddressDisplayName,
ContentStream = new MemoryStream(_pdfCreater.GeneratePDF(otp)),
AttachmentName = faxAsMailRequest.FaxFileName
};
}
catch(Exception ex)
{
Log.Error("Method : BuildEmailInfo. Exception raised while building email data : {@Message}", ex.Message, ex);
}
return email;
}
下面是我的单元测试代码,每当我执行它时,它都会抛出一个错误 Expected invocation on the mock at least once, but was never performed: x=>x.GeneratePDF(pdfdata)。另外让我知道执行测试的方法是否正确
public class SMTPEmailSenderTest
{
private SMTPEmailSender _sMTPEmailSender;
Mock<IPDFCreater> _mockPdfCreator;
public SMTPEmailSenderTest()
{
_mockPdfCreator = new Mock<IPDFCreater>();
_sMTPEmailSender = new SMTPEmailSender(_mockPdfCreator.Object);
}
[Theory]
[MemberData(nameof(GetFaxAsMailObject))]
public void BuildEmailInfoTest_ReturnsValidEmailObject(FaxMailDTO faxMailDTO)
{
string smpturl = "localhost";
var otp = new PDFData { OTP = faxMailDTO.OTP };
var result = _sMTPEmailSender.BuildEmailInfo(smpturl, faxMailDTO);
_mockPdfCreator.Verify(x => x.GeneratePDF(otp));
}
}
模拟对象的验证应该是您在单元测试中的'last resort'。想一想:如果 PDF 创建者不调用 GeneratePDF 方法,是否违反了实际要求?用户只关心PDF生成了。
在这种情况下,您可以直接验证BuildEmailInfo方法的结果,例如:
var result = _sMTPEmailSender.BuildEmailInfo(smpturl, faxMailDTO);
var expectedBytes = ...; // TODO - get the expected byte[] array from somewhere
Assert.Equal(expectedBytes, result.ContentStream.ToArray());
此外,您也许可以在完全不模拟依赖项的情况下编写此测试?如果可以调用实际的 PDF creator 对象在内存中生成 byte[] 数组,则可以使用真实对象而不是模拟它。
这一行:
_mockPdfCreator.Verify(x => x.GeneratePDF(otp));
执行 'verification'。这是一个断言,用于检查方法 .GeneratePDF
是否已在 _mockPdfCreator
上以 otp
作为参数调用。
Mock 对象接口中的所有 .Verify 方法都用于检查是否调用了某些方法或 属性。你还可以提供一些过滤器来查看是否传递了某些参数,例如:
_myMock.Verify(x => x.FooBar(5));
_myMock.Verify(x => x.FooBar(123));
_myMock.Verify(x => x.FooBar(It.IsAny<int>());
_myMock.Verify(x => x.FooBar(It.Is<int>(number => (number-5)%3 > 10));
所有这些都检查 FooBar
是否在 _myMock
上,但它们每个都只查看使用特定参数值的调用:5、123、anything-that-is-int 或(...).
您不能使用 .Verify 检查 return 值。
那里没有这样的选项。
为什么?想想看。你有:
_mockPdfCreator = new Mock<IPDFCreater>();
....
_mockPdfCreator.Verify(x => x.GeneratePDF(otp));
_mockPdfCreator
是您的模拟对象。不是真的。这是一个小幽灵,就像是某个 IPDFCreater 一样。
那里没有丝毫真正的实现。
你怎么能指望 GeneratePDF
return 有意义?
它只是不会。后面什么也没有。如果有任何东西调用该方法 GeneratePDF
,它将 return NULL(或抛出异常,具体取决于模拟模式:Loose/Strict)。
...除非你设置你的模拟来做不同的事情:
var theThing = ...;
_mockPdfCreator = new Mock<IPDFCreater>();
_mockPdfCreator.Setup(x => x.GeneratePDF(It.IsAny<...>())).Returns(theThing);
....
// ... now check what `GeneratePDF` returned?!
现在任何调用GeneratePDF
方法的,都会returntheThing
。好的。
但是你已经知道了。没有什么可检查的。您将 GeneratePDF 设置为 return 的东西,因此没有丝毫检查 GeneratePDF return 的意义。这是您的模拟和设置!
Sooo,如果有任何名为 GeneratePDF 的东西,那么 NULL 将被 returned,因为没有 GeneratePDF 的设置。但是,正如 Verify 所证明的那样,从未调用过 GeneratePDF。这意味着当您创建 SMTPEmailSender 时,将模拟作为参数提供给它:
_mockPdfCreator = new Mock<IPDFCreater>();
_sMTPEmailSender = new SMTPEmailSender(_mockPdfCreator.Object);
然后在测试中你有:
....
var result = _sMTPEmailSender.BuildEmailInfo(smpturl, faxMailDTO);
_mockPdfCreator.Verify(x => x.GeneratePDF(otp));
然后,显然 _sMTPEmailSender.BuildEmailInfo
根本不想调用 GeneratePDF
。
为什么?不知道。很可能 smpturl
或 faxMailDTO
中的某些内容对于此用例被认为是无效的,并且跳过了 generate-pdf 步骤。检查结果。查看是否有任何错误或消息可以告诉您为什么它甚至没有尝试调用 GeneratePDF。
另请注意,您所写的验证是
x => x.GeneratePDF(otp)
这很具体。它有 hard-coded 对 otp
的引用。所以也许它被调用了,但是参数值不同?
尝试添加:
var result = _sMTPEmailSender.BuildEmailInfo(smpturl, faxMailDTO);
_mockPdfCreator.Verify(x => x.GeneratePDF(It.IsAny<PDFData>())); // <-
_mockPdfCreator.Verify(x => x.GeneratePDF(otp));
或类似的东西,看看哪个验证失败。如果前者通过而后者失败,那么一切都很好,只是不是您期望的确切 OTP(也许 _sMTPEmailSender 克隆了它?等等)。
万一前者失败了,那就意味着 GeneratePDF
真的连一次都没有被调用,然后这意味着你必须了解为什么 BuildEmailInfo
带有参数(smpturl, faxMailDTO) 没有达到您的预期。你那里有一个 try-catch-log。也许一些空引用表达式?但我对此表示怀疑。
你已经做到了:
[MemberData(nameof(GetFaxAsMailObject))] /// <==== B
public void BuildEmailInfoTest_ReturnsValidEmailObject(FaxMailDTO faxMailDTO) // <--- A
{
...
var otp = new PDFData { OTP = faxMailDTO.OTP }; //<--- C
...
_mockPdfCreator.Verify(x => x.GeneratePDF(otp)); //<---D
因此,faxMailDTO
来自 GetFaxAsMailObject。 BuildEmailInfo 通过参数获取它并将它的一部分传递给 GeneratePDF。然后你在 Verify that D uses newly-constructed otp
中断言来自 C 行。那是行不通的。来自 A+B 的 faxMailDTO
所以来自 GetFaxAsMailObject
当然不包含来自 C 的 otp
并且当然不会将 otp
对象传递给 GeneratePDF。 GeneratePDF 将从 faxMailDTO
来自 A+B.
我想我已经说得够多了,涵盖了您的测试设置的所有问题。您几乎是对的。祝你好运!