Изучаем TDD, всегда сталкиваемся с круговой зависимостью - PullRequest
5 голосов
/ 09 марта 2012

Я начал использовать TDD, чтобы улучшить свое качество и дизайн своего кода, но обычно я сталкиваюсь с проблемой.Я попытаюсь объяснить это на простом примере: я пытаюсь реализовать простое приложение с использованием дизайна пассивного представления.Это означает, что я стараюсь сделать вид максимально тупым.Давайте рассмотрим приложение, в котором в графическом интерфейсе есть кнопка и метка.Если пользователь нажимает кнопку, создается файл с одной случайной строкой.Затем метка показывает, было ли создание успешным или нет.Код может выглядеть следующим образом:

  • Интерфейс IView: свойство строки одиночного установщика: Результат
  • Класс GUIEventListener: метод OnUserButtonClick, который вызывается из кнопки графического интерфейса пользователя
  • Класс FileSaver: метод SaveFile, который вызывается из GUIEventListener
  • Класс GUIController: метод UpdateLabel, который вызывается из метода SaveFile класса FileSaver с параметром, зависящим от успеха метода SaveFile.

Экземпляр выглядит следующим образом:

  • ctor: View (GUIEventListener eventListener)
  • ctor GUIEventListener: GUIEventListener (FileSaver fileSaver)
  • Контроллер FileSaver: FileSaver контроллер (GUICont)
  • ctor GUIController: GUIController (представление View)

Как вы можете ясно видеть, в дизайне есть круговая зависимость.Я обычно стараюсь избегать использования событий, мне не нравится тестировать с ними, и я думаю, что этот тип дизайна более понятен, поскольку в нем четко указано, каково отношение классов.Я слышал о стиле дизайна IoC, но я не очень знаком с ним.

В чем заключается моя "точка отсчета" в TDD по этому вопросу?Я всегда сталкиваюсь с этой проблемой и хочу выучить правильный шаблон или принцип, чтобы избежать этого в будущем.

Ответы [ 3 ]

3 голосов
/ 09 марта 2012
  • Класс GUIController: метод UpdateLabel, который вызывается из класса SaveFile

...

  • Файл FileSaver: FileSaver (контроллер GUIController)

Вот недостаток в вашем дизайне. FileSaver не должен зависеть от того, кто его вызывает (читай: не должен содержать ссылку на слой под ним), он должен просто выполнять свою работу, т.е. сохранять файл и информировать мир о том, как прошла операция - обычно через возвращаемое значение.

Это на самом деле не имеет отношения к TDD, за исключением того, что, возможно, TDD заставил бы вас думать с точки зрения самого основного поведения, которое ожидается от FileSaver, и осознавать, что не несет ответственности за обновление метки (см. Single Responsibility Принцип ).

Что касается других частей вашей системы, то, как сказал Рой, их чаще всего будет трудно тестировать в TDD, за исключением контроллера.

2 голосов
/ 09 марта 2012

Модульное тестирование пользовательского интерфейса часто является проблемой по многим причинам ... То, как я делал это в последние несколько лет в проектах MVC, заключается в простом модульном тестировании только контроллеров и последующем тестировании приложения на практике..

Контроллеры могут быть легко протестированы модульно, потому что они являются классами логики, как и любые другие, и вы можете смоделировать зависимости.Интерфейсы, особенно для веб-приложений, намного сложнее.Вы можете использовать такие инструменты, как Selenium или WatiN , но это действительно интеграционное / приемочное тестирование, а не модульное тестирование.

Вот некоторые дальнейшие чтения:

Как начать работу с Selenium Core и ASP.NET MVC

Именно так действия контроллера ASP.NET MVC должны тестироваться модульно

Удачи!

0 голосов
/ 09 марта 2012

Я бы избавился от класса GUIEventListener. Мне кажется это излишним.

Поскольку представление знает, когда нажимается кнопка, пусть представление делится своими знаниями со всем миром:

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

FileSaver еще проще:

public interface IFileSaver
{
    Boolean SaveFileWithRandomLine(); 
}

Ради интереса, давайте создадим интерфейс для контроллера:

public interface IController
{ 

}

И реализация контроллера:

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

    }
}

ОК, давайте напишем тесты (я использую 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!"));
    }
}

Я думаю, что тесты довольно ясны, но я не знаю, знаете ли вы Moq.

Полный код для контроллера может выглядеть следующим образом (я просто собрал его в одну строку, но это не обязательно, конечно):

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

Как вы можете видеть, таким образом вы можете протестировать контроллер, и у нас даже нет возможности запустить View или FileSaver. Используя интерфейсы, они не должны знать друг друга.

Представление ничего не знает (за исключением того, что кто-то может быть проинформирован о нажатии кнопки), это максимально возможный дамп. Обратите внимание, что никакие события не загрязняют интерфейс, но если вы собираетесь реализовать View в WinForms, ничто не мешает вам использовать события внутри View-реализации. Но никто снаружи не должен знать, и поэтому нам не нужно проверять это.

FileSaver просто сохраняет файлы и сообщает, произошел ли сбой или нет. Он не знает о контроллерах и представлениях.

Контроллер собирает все вместе, не зная о реализациях. Он просто знает контракты. Он знает о View и FileSaver.

С этим дизайном мы просто тестируем поведение контроллера. Мы спрашиваем: «Если кнопка была нажата, было ли представление информировано о том, что оно должно отображать эту информацию?» и так далее. Если хотите, вы можете добавить дополнительные тесты, чтобы проверить, был ли метод сохранения на FileSaver вызван контроллером.

Хорошим ресурсом по этой теме является Джереми Миллер * «Построй свою собственную серию CAB»

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...