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