学习TDD,总是遇到循环依赖

SLO*_*OBY 5 c# oop tdd

我开始使用TDD来提高我的质量和代码设计,但我经常遇到问题.我将尝试通过一个简单的例子来解释它:我尝试使用被动视图设计实现一个简单的应用程序.这意味着我尽量使视图变得愚蠢.让我们考虑一个应用程序,其中GUI有一个按钮和一个标签.如果用户按下按钮,则会在其中创建一个随机行的文件.然后标签显示创建是否成功.代码可能如下所示:

  • IView接口:单个setter字符串属性:Result
  • GUIEventListener类:从GUI的按钮调用的OnUserButtonClick方法
  • FileSaver类:从GUIEventListener调用的SaveFile方法
  • GUIController类:UpdateLabel方法,它从FileSaver类的SaveFile方法调用,其参数取决于SaveFile方法的成功.

实例化看起来像这样:

  • View的ctor:View(GUIEventListener eventListener)
  • GUIEventListener的ctor:GUIEventListener(FileSaver fileSaver)
  • FileSaver的ctor:FileSaver(GUIController控制器)
  • GUIController的ctor:GUIController(查看视图)

您可以清楚地看到,设计中存在循环依赖关系.我经常试图避免使用事件,我不喜欢用它们进行测试,我认为这种类型的设计更加自我解释,因为它清楚地说明了类的关系.我听说过IoC的设计风格,但我并不熟悉它.

关于这个问题,我在TDD中的"关键点"是什么?我总是遇到这个问题,我想学习一个适当的模式或原则,以避免将来.

slo*_*oth 0

我会摆脱 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 系列》