Как избежать доступа в тест-ориентированной разработке? - PullRequest
15 голосов
/ 28 сентября 2010

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

Вот пример, упрощенный код рисования объекта без TDD:

class Entity {
    private int x;
    private int y;
    private int width;
    private int height;

    void draw(Graphics g) {
        g.drawRect(x, y, width, height);
    }
}

Сущность знает, как рисовать себя, это хорошо.Все в одном месте.Тем не менее, я делаю TDD, поэтому я хочу проверить, правильно ли был перемещен мой объект методом fall (), который я собираюсь реализовать.Вот как может выглядеть контрольный пример:

@Test
public void entityFalls() {
    Entity e = new Entity();
    int previousY = e.getY();
    e.fall();
    assertTrue(previousY < e.getY());
}

Мне нужно взглянуть на внутреннее (по крайней мере, логически) состояние объекта и посмотреть, правильно ли обновлена ​​позиция.Поскольку это на самом деле мешает (я не хочу, чтобы мои тестовые примеры зависели от моей графической библиотеки), я переместил код рисования в класс "Renderer":

class Renderer {
    void drawEntity(Graphics g, Entity e) {
        g.drawRect(e.getX(), e.getY(), e.getWidth(), e.getHeight());
    }
}

Слабосвязанное, хорошо.Я даже могу заменить средство визуализации на то, которое отображает сущность совершенно другим способом.Однако мне пришлось раскрыть внутреннее состояние объекта, а именно средства доступа для всех его свойств, чтобы средство визуализации могло его прочитать.

Я чувствую, что это было специально вызвано TDD.Что я могу сделать по этому поводу?Мой дизайн приемлем?Нужно ли Java ключевое слово "friend" из C ++?

Обновление:

Спасибо за ваш ценный вклад!Однако я боюсь, что выбрал плохой пример для иллюстрации своей проблемы.Это было полностью сделано, сейчас я продемонстрирую тот, который ближе к моему фактическому коду:

@Test
public void entityFalls() {
    game.tick();
    Entity initialEntity = mockRenderer.currentEntity;
    int numTicks = mockRenderer.gameArea.height
                   - mockRenderer.currentEntity.getHeight();
    for (int i = 0; i < numTicks; i++)
        game.tick();
    assertSame(initialEntity, mockRenderer.currentEntity);
    game.tick();
    assertNotSame(initialEntity, mockRenderer.currentEntity);
    assertEquals(initialEntity.getY() + initialEntity.getHeight(),
                 mockRenderer.gameArea.height);
}

Это реализация игры, основанная на игровом цикле, где сущность может упасть, среди прочего.Если он падает на землю, создается новая сущность.

«mockRenderer» - это ложная реализация интерфейса «Renderer».Этот дизайн был частично вызван TDD, но также из-за того, что я собираюсь написать пользовательский интерфейс в GWT, и нет явного рисования в браузере (пока), поэтому я не думаю, что это возможно дляСущность класса, чтобы взять на себя эту ответственность.Кроме того, я хотел бы сохранить возможность портирования игры на нативный Java / Swing в будущем.

Обновление 2:

Думая об этом еще немного, возможноэто нормально, как это.Может быть, это нормально, что сущность и рисунок разделены и что сущность достаточно рассказывает другим объектам о себе, чтобы нарисовать.Я имею в виду, как еще я мог достичь этого разделения?И я не вижу, как жить без этого.Даже великие объектно-ориентированные программисты иногда используют объекты с геттерами / сеттерами, особенно для чего-то вроде объекта сущности.Может быть, геттер / сеттер не все злые.Что ты думаешь?

Ответы [ 6 ]

5 голосов
/ 28 сентября 2010

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

Вы можете изменить код, приведенный выше, чтобы сущность выполняла рисование, что полезно, если сущность не прямоугольник, а фактически круг.

void Entity::draw(Graphics g) {
     g.drawRect(x,y, width, height);
} 

Затем вы проверите, что в ваших тестах g вызывали правильные методы.

3 голосов
/ 28 сентября 2010

Вы говорите, что чувствуете, что класс Renderer, который вы придумали, был "специально вызван" TDD.Итак, давайте посмотрим, куда TDD привел вас.От класса Rectangle, который отвечал за свои координаты и за сам отрисовку, до класса Rectangle, который имеет единственную ответственность за поддержание своих координат, и рендерера, который имеет единственную ответственность за рендеринг.Вот что мы имеем в виду, когда говорим «Test Driven» - эта практика влияет на ваш дизайн.В этом случае он привел вас к дизайну, который более точно соответствовал Принципу единой ответственности - дизайну, который вы бы не использовали без испытаний.Я думаю, что это хорошо.Я думаю, что вы хорошо практикуете TDD, и я думаю, что это работает для вас.

2 голосов
/ 28 сентября 2010

Итак, если бы вы не переместили метод draw(Graphics) из Entity, у вас был бы отлично тестируемый код.Вам нужно только внедрить реализацию Graphics, которая сообщает о внутреннем состоянии Entity в ваш тестовый комплект.Только мое мнение, хотя.

1 голос
/ 28 сентября 2010

Во-первых, знаете ли вы о классе java.awt.Rectangle, как именно эта проблема решается в библиотеке времени выполнения Java?

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

Это очень важно при разработке API, и, скорее всего, именно поэтому вы обнаружили, что ваш результат слабо связан.

Второе значение заключается в том, что ваши тесты являются «живыми комментариями», а не древними, нетронутыми комментариями.Тест показывает, как должен вызываться ваш код, и вы можете мгновенно убедиться, что он ведет себя так, как указано.Это не имеет отношения к тому, что вы спрашиваете, но должно продемонстрировать, что у тестов есть больше целей, чем просто слепо вызывать код, который вы написали некоторое время назад.

0 голосов
/ 28 сентября 2010

Я думаю, что ваш пример имеет много общего с примером, используемым здесь:

http://www.m3p.co.uk/blog/2009/03/08/mock-roles-not-objects-live-and-in-person/

Используя ваш оригинальный пример и заменяя Entity на Hero, fall () на jumpFrom (Balcony), и draw () как moveTo (Room) становится удивительно похожим.Если вы используете подход фиктивного объекта, который предлагает Стив Фримен, ваша первая реализация была не так уж и плоха.Я считаю, что @Colin Хеберт дал лучший ответ, когда он указал в этом направлении.Здесь не нужно ничего выставлять.Вы используете фиктивные объекты, чтобы проверить, имело ли место поведение героя.

Обратите внимание, что автор статьи написал соавторскую книгу, которая может вам помочь:

http://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627

Есть несколько хороших статей, свободно доступных авторами в формате PDF, об использовании макетов объектов для управления вашим дизайном в TDD.

0 голосов
/ 28 сентября 2010

То, что вы хотите проверить, это то, как ваш объект будет реагировать на определенные вызовы, а не то, как он работает внутри.

Так что на самом деле нет необходимости (и это было бы плохой идеей) для доступа к недоступным полям / методам.

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

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