Как избежать доступа в процессе тестирования?

Я изучаю развитие, основанное на тестах, и я заметил, что он заставляет свободно связанные объекты, что в принципе хорошо. Однако это также иногда заставляет меня предоставлять аксессуры для свойств, которые мне не нужны в обычном режиме, и я думаю, что большинство людей на 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" из С++?

Update:

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

@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, и в браузере пока нет явного чертежа, поэтому я не думаю, что это возможно для класс Entity, чтобы взять на себя эту ответственность. Кроме того, я хотел бы сохранить возможность переноса игры на родную Java/Swing в будущем.

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

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

Ответы

Ответ 1

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

Ответ 2

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

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

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

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

Ответ 3

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

Ответ 4

В первую очередь, знаете ли вы о классе java.awt.Rectangle, как эта точная проблема рассматривается в Java Runtime Library?

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

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

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

Ответ 5

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

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

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

Ответ 6

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

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

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

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

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

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