我以为我会尝试在命令行上模拟ATM机的代码kata。我决定使用TDD来推动设计。我遇到了一个有趣的场景,我很好奇其他人在这种情况下所做的事情。
如果你在我的AtmMachine课上看(在底部),你会注意到我正在故意退出我的while循环,这样我的测试就不会超时。这对我来说就像一个代码味道,我想知道其他人是否做过这样的事情。
我目前的感受被分开了:
以下是迄今为止我为atm机器进行的单元测试:
[TestClass]
public class when_atm_starts
{
private static readonly string WELCOME_MSG = "Welcome to Al Banco de Ruiz!";
private AtmMachine _atm;
private Mock<IAtmInput> _inputMock;
private Mock<IAtmOutput> _outputMock;
private Mock<ILogger> _loggerMock;
private Mock<ICommandFactory> _cmdFactoryMock;
[TestInitialize]
public void BeforeEachTest()
{
_inputMock = new Mock<IAtmInput>();
_outputMock = new Mock<IAtmOutput>();
_loggerMock = new Mock<ILogger>();
_cmdFactoryMock = new Mock<ICommandFactory>();
_atm = new AtmMachine(_inputMock.Object, _outputMock.Object, _loggerMock.Object, _cmdFactoryMock.Object);
}
[TestMethod]
public void no_one_should_be_logged_in()
{
this.SetupForCancelledUser();
_atm.Start();
Assert.IsNull(_atm.CurrentUser);
}
[TestMethod]
public void should_print_welcome_to_output()
{
this.SetupForCancelledUser();
_atm.Start();
_outputMock.Verify(o => o.Write(WELCOME_MSG));
}
[TestMethod]
public void should_execute_login_command()
{
Mock<ILoginCommand> loginCmdMock = new Mock<ILoginCommand>();
_cmdFactoryMock.Setup(cf => cf.GetLoginCommand(_inputMock.Object, _outputMock.Object))
.Returns(loginCmdMock.Object);
loginCmdMock.Setup(lc => lc.LogonUser())
.Returns(AtmUser.CancelledUser);
_atm.Start();
loginCmdMock.Verify(lc => lc.LogonUser());
}
private void SetupForCancelledUser()
{
Mock<ILoginCommand> loginCmdMock = new Mock<ILoginCommand>();
_cmdFactoryMock.Setup(cf => cf.GetLoginCommand(_inputMock.Object, _outputMock.Object))
.Returns(loginCmdMock.Object);
loginCmdMock.Setup(lc => lc.LogonUser())
.Returns(AtmUser.CancelledUser);
}
}
这是相应的AtmMachine课程。
public class AtmMachine
{
public static readonly string WELCOME_MSG = "Welcome to Al Banco de Ruiz!";
private bool _shouldContinue;
private ILogger _log;
private ICommandFactory _cmdFactory;
private IAtmInput _input;
private IAtmOutput _output;
public object CurrentUser { get; set; }
public AtmMachine(
IAtmInput input,
IAtmOutput output,
ILogger logger,
ICommandFactory cmdFactory)
{
this._input = input;
this._output = output;
this._log = logger;
this._cmdFactory = cmdFactory;
}
public void Start()
{
_shouldContinue = true;
while (_shouldContinue)
{
_output.Clear();
_output.Write(WELCOME_MSG);
AtmUser user = this.GetNextUser();
if (user == AtmUser.CancelledUser) { _shouldContinue = false; }
_shouldContinue = false;
}
}
private AtmUser GetNextUser()
{
ILoginCommand loginCmd = _cmdFactory.GetLoginCommand(_input, _output);
return loginCmd.LogonUser();
}
}
答案 0 :(得分:11)
以这种方式测试循环是正确的。除了“没有人登录”之外,我没有太多关于你试图试驾的功能的背景,所以我会对我的建议采取一些自由。
首先,对于循环测试,您可以通过几种方式执行此操作,具体取决于您的偏好。第一种方法是将循环条件提取到一个可以在用于测试的子类中重写的方法。
// In your AtmMachine
public void Start()
{
_shouldContinue = true;
while (stillRunning())
{
// Do some ATM type stuff
}
}
protected virtual bool stillRunning() {
return _shouldContinue;
}
在测试中,您可以创建一个覆盖stillRunning()
的特殊测试类。
// Inside your test
[TestMethod]
public void no_one_should_be_logged_in()
{
_atm = new AtmThatImmediatelyShutsDown();
this.SetupForCancelledUser();
_atm.Start();
Assert.IsNull(_atm.CurrentUser);
}
class AtmThatImmediatelyShutsDown : AtmMachine {
protected override bool stillRunning() {
return false;
}
}
另一种选择是将条件注入可以模拟的类/接口。选择是你的。
其次,为简单起见,我会将该循环的内容提取到一个具有更高可见性的方法中,以允许在循环中测试代码。
// In your AtmMachine
public void Start()
{
_shouldContinue = true;
while (stillRunning())
{
processCommands();
}
}
public void processCommands() {
...
}
现在,您可以直接调用processCommands()
方法并跳过所有循环。
希望有所帮助!
布兰登
答案 1 :(得分:2)
似乎是一个应用SingleResponsibilityPrinciple的地方。
如果将这些不相关的职责分成两个角色,那么测试它们就会变得容易得多。
public void Start()
{
while (_shouldContinue)
{
_output.Clear();
_output.Write(WELCOME_MSG);
if (HasUserLoggedIn)
SomeOtherType.ProcessTransaction();
}
}