我开始使用TDD来提高我的质量和代码设计,但我经常遇到问题.我将尝试通过一个简单的例子来解释它:我尝试使用被动视图设计实现一个简单的应用程序.这意味着我尽量使视图变得愚蠢.让我们考虑一个应用程序,其中GUI有一个按钮和一个标签.如果用户按下按钮,则会在其中创建一个随机行的文件.然后标签显示创建是否成功.代码可能如下所示:
实例化看起来像这样:
您可以清楚地看到,设计中存在循环依赖关系.我经常试图避免使用事件,我不喜欢用它们进行测试,我认为这种类型的设计更加自我解释,因为它清楚地说明了类的关系.我听说过IoC的设计风格,但我并不熟悉它.
关于这个问题,我在TDD中的"关键点"是什么?我总是遇到这个问题,我想学习一个适当的模式或原则,以避免将来.
我会摆脱 GUIEventListener 类。对我来说似乎有点过分了。
由于视图知道按钮何时被单击,因此让视图与世界分享它的知识:
public interface IView
{
void DisplayMessage(string message);
void AddButtonClickHandler(Action handler);
}
Run Code Online (Sandbox Code Playgroud)
FileSaver 更简单:
public interface IFileSaver
{
Boolean SaveFileWithRandomLine();
}
Run Code Online (Sandbox Code Playgroud)
只是为了好玩,让我们为控制器创建一个接口:
public interface IController
{
}
Run Code Online (Sandbox Code Playgroud)
以及控制器的实现:
public class Controller : IController
{
public Controller(IView view, IFileSaver fileSaver)
{
}
}
Run Code Online (Sandbox Code Playgroud)
好的,让我们编写测试(我使用 NUnit 和 Moq):
[TestFixture]
public class ControllerTest
{
private Controller controller;
private Mock<IFileSaver> fileSaver;
private Mock<IView> view;
private Action ButtonClickAction;
[SetUp]
public void SetUp()
{
view = new Mock<IView>();
//Let's store the delegate added to the view so we can invoke it later,
//simulating a click on the button
view.Setup((v) => v.AddButtonClickHandler(It.IsAny<Action>()))
.Callback<Action>((a) => ButtonClickAction = a);
fileSaver = new Mock<IFileSaver>();
controller = new Controller(view.Object, fileSaver.Object);
//This tests if a handler was added via AddButtonClickHandler
//via the Controller ctor.
view.VerifyAll();
}
[Test]
public void No_button_click_nothing_happens()
{
fileSaver.Setup(f => f.SaveFileWithRandomLine()).Returns(true);
view.Verify(v => v.DisplayMessage(It.IsAny<String>()), Times.Never());
}
[Test]
public void Say_it_worked()
{
fileSaver.Setup(f => f.SaveFileWithRandomLine()).Returns(true);
ButtonClickAction();
view.Verify(v => v.DisplayMessage("It worked!"));
}
[Test]
public void Say_it_failed()
{
fileSaver.Setup(f => f.SaveFileWithRandomLine()).Returns(false);
ButtonClickAction();
view.Verify(v => v.DisplayMessage("It failed!"));
}
}
Run Code Online (Sandbox Code Playgroud)
我认为测试已经很清楚了,但我不知道你是否知道Moq。
控制器的完整代码可能如下所示(我只是将其写成一行,但当然您不必这样做):
public class Controller : IController
{
public Controller(IView view, IFileSaver fileSaver)
{
view.AddButtonClickHandler(() => view.DisplayMessage(fileSaver.SaveFileWithRandomLine() ? "It worked!" : "It failed!"));
}
}
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,通过这种方式您可以测试控制器,而我们甚至没有启动来实现 View 或 FileSaver。通过使用接口,他们不必互相了解。
视图什么都不知道(除了单击按钮时可能会通知某人),它尽可能转储。请注意,没有事件会污染界面,但如果您要在 WinForms 中实现视图,则没有什么可以阻止您在视图实现中使用事件。但外界没有人知道,所以我们不需要测试它。
FileSaver 只是保存文件并告诉它是否失败。它不知道控制器和视图。
控制器将所有内容放在一起,而不了解实现。它只知道合同。它了解 View 和 FileSaver。
通过这种设计,我们只需测试控制器的行为。我们问:“如果单击该按钮,视图是否被告知应该显示该信息?” 等等。如果您愿意,您可以添加更多测试来检查控制器是否调用了 FileSaver 上的 Save-Method。
关于此主题的一个很好的资源是杰里米·米勒 (Jeremy Miller) 的《构建您自己的 CAB 系列》