是否有任何地方或任何人可以用简单的英语而不是“根据自己定义术语”来解释这是如何工作的?
4 回答
所以,你有一个依赖于其他东西的类。
让我们打个比方:汽车需要引擎。
汽车依赖于发动机。测试汽车和发动机是否一起工作很容易,但是如果测试没有发动机的汽车,或者汽车正确地“调用”发动机呢?我们能做的就是用一些东西(模拟)代替发动机,然后按下油门(打电话),并验证假(模拟)发动机是否收到了正确的节气门体输入。您没有验证整个系统,而是通过使用模拟对象进行测量,单独测试了您想要的东西。
它在实践中变得更加复杂和强大,但是......
如果你为你的类编写单元测试,在某些时候你会遇到你的测试执行代码调用外部资源的情况。大多数情况下,这是一个数据库,但其他数据库也是可能的。我通常使用信用卡计费服务提供商作为示例,因为在这种情况下,您显然不想在每次运行测试时实际调用该服务。
在这种情况下,通常将服务对象替换为一种不使用任何资源的虚假服务。这称为存根或模拟对象。存根和模拟之间的区别似乎是一些讨论的主题,但本质上它们是相同的。
作为 Rhino Mock 的 Mocking 框架可帮助您创建模拟对象,这些对象会按照您对实际服务的期望进行响应。您可以将其与每次执行测试时可以重播的服务调用的实际响应记录进行比较。
要真正理解 Mocks,首先必须掌握四个概念
什么是交互测试
什么是隔离框架(如 rhino mocks)
什么是假货
什么是存根
最后什么是模拟
交互测试是关于检查特定方法(您正在测试的方法)是否使用它应该使用的参数调用另一个类(外部依赖项)中的另一个方法。想象一下,您在某个类中有一个方法,每次使用无效参数调用它时都会记录下来。为了阐明存根和模拟之间的区别,我添加了 2 个外部依赖项(IStringAnalyzer 和 ILoger):
class SomeClass
{
IStringAnalyzer stringAnalizer;
ILogger logger;
public SomeClass(IStringAnalyzer stringAnalyzer, ILogger logger)
{
this.logger = logger;
this.stringAnalyzer = stringAnalyzer;
}
public void SomeMethod(string someParameter)
{
if (stringAnalyzer.IsValid(someParameter))
{
logger.Log("Invalid string");
}else
{
//do something with someParameter
}
}
}
在此示例中,您要测试对 SomeClass 的 SomeMethod 的方法调用是否使用无效参数调用 ILogger 中使用字符串参数“Invalid string”的 log 方法。您不想使用 IStringAnalyzer 和 ILogger 的“真实”实现,因为它们可能有错误,并且因为这是单元测试,您只想一次测试一件事,如果您测试如何一次测试几件事情真正做的是集成测试。一次只测试一件事的原因是,如果您的测试失败,您会立即知道它失败了,因为您正在测试的只有那件事。
您需要提供两种替代实现,一种用于 IStringAnalyzer,另一种用于 ILogger,以便您可以正确执行此测试。这些替代实现在他们需要做什么方面会有所不同。对于 IStringAnalyzer,您只希望它在被调用时返回 false,以便被测方法将通过您要测试的代码路径。你真的不关心参数的值(someParameter)。
对于 ILogger 的 log 方法,您想知道它是否被调用,并且它是用“无效字符串”调用的,以便您可以在测试中断言它。
IStringAnalyzer 和 ILogger 的这两种替代实现都被称为“假货”(因为它们伪造了外部依赖项),但一种是存根(IStringAnalyzer),另一种是模拟(ILogger)。存根只是为了让您到达您需要进行测试的地方(在这种情况下,IStringAnalyzer 的 IsValid 方法返回 false)。模拟可以让您检查与外部依赖项的交互是否正确完成(在本例中为 ILogger)。有些人将模拟(或此类模拟)称为测试间谍(我认为这是一个更好的名字)。是的,还有其他类型的模拟(虽然我从未使用过它们)。一个很好的来源是使用 Michael Feathers 和 Roy Osherove 的 The art of unit testing 的遗留代码。
您可以“手动”创建存根和模拟,例如:
class StringAnalyzerStub : IStringAnalyzer
{
public bool whatToReturn;
public StubStringAnalyzerStub(bool whatToReturn)
{
this.whatToReturn = whatToReturn;
}
public bool IsValid(string parameter)
{
return whatToReturn;
}
}
class LoggerMock : ILogger
{
public string WhatWasPassedIn;
public void Log(string message)
{
WhatWasPassedIn = message;
}
}
这是测试:
[Test]
public void SomeMethod_InvalidParameter_CallsLogger
{
IStringAnalyzer s = new StringAnalyzerStub(false); //will always return false when IsValid is called
ILogger l = new LoggerMock();
SomeClass someClass = new SomeClass(s, l);
someClass.SomeMethod("What you put here doesnt really matter because the stub will always return false");
Assert.AreEqual(l.WhatWasPassedIn, "Invalid string");
}
手动执行此操作的问题是容易出错且难以维护,因此需要像 Rhino Mocks 这样的隔离框架。它们允许您动态创建这些模拟和存根,这是使用 Rhino Mocks 进行相同测试的样子(使用排列、行为、断言语法):
[Test]
public void SomeMethod_InvalidParameter_CallsLogger
{
Rhino.Mocks.MockRepository mockRepository = new Rhino.Mocks.MockRepository();
IStringAnalyzer s = mockRepository.Stub<IStringRepository>();
s.Expect(s => s.IsValid("something, doesnt matter").IgnoreParameters().Return(false);
ILogger l = mockRepository.DynamicMock<ILogger>();
l.Log("Invalid string");
SomeClass someClass = new SomeClass(s, l);
mockRepository.ReplayAll();
someClass.SomeMethod("What you put here doesnt really matter because the stub will always return false");
l.AssertWasCalled(l => l.Log("Invalid string"));
}
没有根据自己定义的术语:)
免责声明:我在文本编辑器中编写了所有这些,因此代码中可能存在一些语法错误......
模拟是您可以指示以某种方式表现的代理。这对于测试您希望通过使用模拟对象切换实际实例来消除依赖关系的位置很有用。
与存根模拟不同的是,模拟跟踪实际使用情况,因此您的测试可能会验证模拟是否按预期使用。
更多信息请参阅 Fowler 的文章。