当前位置: 动力学知识库 > 问答 > 编程问答 >

c# - Learning TDD, always running into circular dependency

问题描述:

I started using TDD to improve my quality and the design of my code but I usually encounter a problem. I'll try to explain it through a simple example:

I try to implement a simple application using passive view design. This means that I try to make the view as dumb as possible. Let's consider an application, where the GUI has a button and a label. If the user presses the button, a file get created with one random line in it. Then the label displays whether the creation was successful or not.

The code might look like this:

  • IView interface: a single setter string property: Result
  • GUIEventListener class: OnUserButtonClick method which gets called from the GUI's button
  • FileSaver class: SaveFile method which gets called from the GUIEventListener
  • GUIController class: UpdateLabel method which gets called from the FileSaver class's SaveFile method with a parameter depending the success of the SaveFile method.

Instantiation looks like this:

  • View's ctor: View(GUIEventListener eventListener)
  • GUIEventListener's ctor: GUIEventListener(FileSaver fileSaver)
  • FileSaver's ctor: FileSaver(GUIController controller)
  • GUIController's ctor: GUIController(View view)

As you can clearly see, there's a circular dependency in the design.

I usually try to avoid using events, I don't like testing with them and I think this type of design is more self explanatory as it clearly states what are the relation of the classes.

I'v heard of IoC design style but I'm not really familiar with it.

What are my "sticking point" in TDD regarding this issue? I always end up running into this problem and I want to learn a proper pattern or principle to avoid it in the future.

网友答案:

I would get rid of the GUIEventListener class. Seems like a overkill to me.

Since the view knows when the button is clicked, let the view share its knowledge with the world:

public interface IView
{
    void DisplayMessage(string message);
    void AddButtonClickHandler(Action handler);
}

The FileSaver is even simpler:

public interface IFileSaver
{
    Boolean SaveFileWithRandomLine(); 
}

Just for fun, let's create an interface for the controller:

public interface IController
{ 

}

And the controller implementation:

public class Controller : IController
{
    public Controller(IView view, IFileSaver fileSaver)
    {

    }
}

OK, let's write the tests (I am using NUnit and 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!"));
    }
}

I think the tests are pretty clear, but I don't know if you know Moq.

The full code for the Controller could look like the following (I just hammered it into one line, but you don't have to, of course):

public class Controller : IController
{
    public Controller(IView view, IFileSaver fileSaver)
    {
        view.AddButtonClickHandler(() => view.DisplayMessage(fileSaver.SaveFileWithRandomLine() ? "It worked!" : "It failed!"));
    }
}

As you can see, this way you are able to test the controller, and we don't have even startet to implement the View or the FileSaver. By using interfaces, they don't have to know each other.

The View knows nothing (except that somebody may be informed when the Button is clicked), it is as dump as possible. Note that no events pollute the interface, but if you're going to implement the View in WinForms, nothing stops you from using events inside the View-implementation. But nobody outside has to know, and so we don't need to test it.

The FileSaver just saves files and tells if it failed or not. It doesn't know about controllers and views.

The Controller puts everything together, without knowing about the implementations. It just knows the contracts. It knows about the View and the FileSaver.

With this design, we just test the behaviour of the controller. We ask: 'If the button was clicked, was the view informed that it should display that information?' and so on. You could add more tests to check if the Save-Method on the FileSaver were called by the Controller if you like.

A nice resource on this topic is The Build Your Own CAB Series by Jeremy Miller

网友答案:
  • GUIController class: UpdateLabel method which gets called from the FileSaver class's SaveFile

...

  • FileSaver's ctor: FileSaver(GUIController controller)

Here's the flaw in your design. The FileSaver should be agnostic of who calls it (read: shouldn't hold a reference to the layer underneath it), it should just do its job i.e. save a file and inform the world how the operation went - typically through a return value.

This not really related to TDD, except maybe TDD would have forced you to think in terms of the most basic behavior that is expected from a FileSaver and realize it is not its responsibility to update a label (see Single Responsibility Principle).

As for the other parts of your system, like Roy said they'll most often be difficult to test in TDD except for the Controller.

网友答案:

Unit testing UIs is often a problem, for many reasons... The way I've done it in the past few years on MVC projects is to simply unit-test only the Controllers and to later test the application hands-on.

Controllers can be unit-tested easily because they are logic classes just like any other and you can mock out the dependencies. UIs, especially for Web applications, are much tougher. You can use tools such as Selenium or WatiN but that is really integration/acceptance testing rather than unit testing.

Here's some further reading:

How to get started with Selenium Core and ASP.NET MVC

This is how ASP.NET MVC controller actions should be unit tested

Good luck!

分享给朋友:
您可能感兴趣的文章:
随机阅读: