如何在单元测试中为受测系统设置调试
本文关键字:系统 设置 调试 单元测试 | 更新日期: 2023-09-27 18:35:04
如何设置调试,以便可以在测试代码下逐步执行系统?
我有这个测试:
[Test]
public void GetDeviceSettings_is_called()
{
//Arrange
var mockDeviceInteractions = new Mock<IDeviceInteractions>();
mockDeviceInteractions.SetupSet(p => p._settingsFileQueue = It.IsAny<string>());
mockDeviceInteractions.Setup(x => x.GetDeviceSettings(It.IsAny<string>(), It.IsAny<string>()));
//Act
mockDeviceInteractions.Object._settingsFileQueue = @"C:'";
mockDeviceInteractions.Object.GetDeviceSettings("123123", "dfgdfg");
//Assert
mockDeviceInteractions.VerifyAll();
}
这是我正在采取行动的方法:
public virtual DeviceSettings GetDeviceSettingsForSerialNumber(string serial) {
//do stuff - and i've put a break point here, but it does not step at the break point
GetDeviceSettings(string1, string2);
//do more stuff
}
我做错了什么?为什么它不停在断点处?
你必须模拟将被测试的类调用的对象,而不是类本身。
下面是一个示例。它假定您可以将依赖项注入到类中。
想象一下,你正在编写一个核控制系统。您希望通过编写单元测试来检查系统是否会指示反应器在需要时执行紧急关闭。
public enum ReactorStatus { Ok, OhDear};
public interface IReactorInteractions {
ReactorStatus ShutDown(bool nicely);
ReactorStatus Status { get; }
}
public interface ICore {
ReactorStatus ShutDown(bool nicely);
ReactorStatus GetStatus();
}
public class ReactorInteractions : IReactorInteractions {
private ICore reactor;
public ReactorInteractions(ICore reactor) {
this.reactor = reactor;
}
public ReactorStatus ShutDown(bool nicely) {
return reactor.ShutDown(nicely);
}
public ReactorStatus Status {
get { return reactor.GetStatus(); }
}
}
因此,您需要测试 ReactorInteractions 类。
为了做到这一点,然后,你模拟它调用的对象,在本例中ICore
.您不想在实际核心上执行操作。这样做肯定是违纪的,至少
您应该将 ICore
mock 的 Object
属性作为构造函数参数传递给 ReactorInteractions 类 - 此属性不是您应该在测试中访问的内容,它仅用于传递给被测试的类 - 被测试类作用于这个"对象", 允许您使用 Setup
和 Verify
:
private Mock<ICore> mockCore;
private IReactorInteractions reactor;
[SetUp]
public void TestSetup() {
mockCore = new Mock<ICore>();
reactor = new ReactorInteractions(mockCore.Object);
}
所以一些示例测试(如果它们是真正的测试,它将验证和检查有价值的东西 - 测试应该检查逻辑,而不是管道。
[Test]
public void ShutDown_Nicely_Should_Pass() {
mockCore.Setup(m => m.ShutDown(true).Returns(ReactorStatus.Ok));
var status = reactor.ShutDown(true);
status.Should().Be(ReactorStatus.Ok);
mockCore.VerifyAll();
}
[Test]
public void ShutDown_Badly_Should_Fail() {
mockCore.Setup(m => m.ShutDown(false).Returns(ReactorStatus.OhDear));
var status = reactor.ShutDown(false);
status.Should().Be(ReactorStatus.OhDear);
mockCore.VerifyAll();
}
请注意,我在测试设置中没有使用It.IsAny<bool>
。对于刚接触模拟的开发人员来说,这种语法非常令人困惑(在野外看到:试图在测试调用中使用It.IsAny
作为参数) - 这是我希望 Moq 作者在文档中强调的内容。It.IsAny 只有在您完全无法控制参数时才应使用。
在您的情况下,无需使用 It.IsAny
- 您确切地知道要传递哪些值:
const string serial = "123456";
mockDeviceInteractions.Setup(m => m.GetDeviceSettingsForSerialNumber(string serial))
.Returns(new DeviceSettings { Serial = serial };
var settings = classUnderTest.GetDeviceSettingsForSerialNumber(serial);
settings.Serial.Should.Be(serial);
然后在测试中,您将检查是否使用了实际值。如果您测试It.IsAny
请记住,代码中处理该值的部分可以替换为随机数生成器,并且单元测试仍将通过。
话虽如此,Moq 有一个限制,即如果列表中的一个参数未知并且必须使用 It.IsAny
那么它们都必须是(我不再使用它了,所以我不知道是否仍然如此,但我似乎记得无论如何你都可以通过使用回调手动验证参数来解决这个问题)
要回答这个问题,
如何设置调试,以便可以在测试代码下单步执行系统?
您不需要执行任何特定的设置,这与调试从 Visual Studio 中的Start
按钮运行的系统没有任何不同。
单元测试实际上只是强调class
的public
API的代码。单元测试和对公共 API 的实际调用之间的唯一区别是,您有一些attributes
要添加到方法中,并且您在类中mock
依赖项。类本身与正常调试时相同,但您有设置方案来处理不同的逻辑,然后您可以正确verify
这些逻辑。
要让您的测试运行器以debug
运行测试,您必须选择它以运行debug
否则大多数运行器默认以基本上release
模式运行。根据您使用的运行器,这是以不同的方式完成的,但通常允许您right-click
测试以debug
并选择Debug the selected Tests
的内容。
回答你的下一个问题,
我做错了什么?为什么它不停在断点处?
运行上述单元测试时,您正在测试实际类,但正在设置实现的逻辑必须正确处理的方案。
主要问题是你看起来不像是在调用你期望运行的方法,测试永远不会调用GetDeviceSettingsForSerialNumber(...)
除非这是由你没有描述的属性的设置调用的。
您实际所做的是模拟被测系统,因此实际上并没有测试实现的代码,而是基本上测试moq
正常工作。这可以通过以下行标识:
mockDeviceInteractions.Setup(x => x.GetDeviceSettings(It.IsAny<string>(), It.IsAny<string>()));
您是在嘲笑对GetDeviceSettings(string, string)
的调用,我不确定此方法的实现,但是如果该方法标记为virtual
moq
将在后台创建一个新的覆盖方法,当提供任何string
作为参数时将调用该方法。
然后,您正在调用:
mockDeviceInteractions.Object.GetDeviceSettings("123123", "dfgdfg");
moq
后台正在接收此调用,并且拦截器将其与其中一个设置调用(见上文)匹配,它将调用既不执行任何操作也不返回任何内容的安装调用。
如果您尝试测试的方法"GetDeviceSettingsForSerialNumber"在设置属性(我希望这不是您要调用的字段)时被调用,则当您使用调用设置属性时,此方法无效:
mockDeviceInteractions.SetupSet(p => p._settingsFileQueue = It.IsAny<string>());
我从未尝试过这个,但我不相信它会在类中运行实际的属性设置器,这可能是您正在寻找的。
如果要将属性设置为具有默认值,则可以说:
mock.SetupProperty(f => f.Name, "foo");
现在我可以解释为什么我觉得你正在测试的内容是不正确的,但我觉得@stuartd答案涵盖了class
应该如何构建依赖关系,你可以模拟这些依赖关系来返回数据,而不是模拟被测系统内的调用。相反,我将向您展示如何使用您刚才的结构来构建您的实际测试。
[Test]
public void GetDeviceSettings_is_called()
{
//Arrange
var mockDeviceInteractions = new Mock<IDeviceInteractions>();
var deviceSettings = new Mock<IDeviceSettings>();
mockDeviceInteractions.Setup(x => x.GetDeviceSettings(@"123123", "dfgdfg"))
.Returns(deviceSettings.Object)
.Verifiable();
//Act
var actual = mockDeviceInteractions.Object.GetDeviceSettingsForSerialNumber(@"123123");
//Assert
Assert.Equal(deviceSettings.Object, actual);
mockDeviceInteractions.Verify();
}
首先,我已经删除了该属性SetupSet
,这不是必需的,如果您想设置实际属性,只需将其设置在对象上,我还删除了正在设置的属性,因为我看不出这如何适合您的测试,除非您期望从您尚未描述的属性 setter 调用该方法。
接下来,我在GetDeviceSettings
Setup
中添加了一个Returns
方法调用,我不确定这返回了什么,但正如您所看到的,它在我的示例中返回了一个IDeviceSettings
,我也将其标记为Verifiable
。
然后,我从您的对象调用实际GetDeviceSettingsForSerialNumber
。你从来没有叫过这个,所以我对你无法命中断点并不感到惊讶。
最后,我断言从调用返回的IDeviceSettings
与GetDeviceSettings
方法返回的相同。我知道这与您的实现略有不同,因为您返回了一个具体的DeviceSettings
但这实际上应该返回一个interface
.
最后,我正在验证mockDeviceInteractions
,正如您在Verify
调用而不是VerifyAll
调用中看到的那样,因为我偶尔会设置一个模拟,其中包含我不需要Verify
的调用,并且只标记我确实想要使用正确的方法调用Verify
的调用。