Что делать, если тесты TDD показывают новую функциональность, которая также необходима для тестов?

13

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

Например, я пишу точечный класс, который имеет функцию WillCollideWith ( LineSegment ) :

public class Point {
    // Point data and constructor ...

    public bool CollidesWithLine(LineSegment lineSegment) {
        Vector PointEndOfMovement = new Vector(Position.X + Velocity.X,
                                               Position.Y + Velocity.Y);
        LineSegment pointPath = new LineSegment(Position, PointEndOfMovement);
        if (lineSegment.Intersects(pointPath)) return true;
        return false;
    }
}

Я писал тест для CollidesWithLine, когда понял, что мне понадобится функция LineSegment.Intersects ( LineSegment ) . Но должен ли я просто остановить то, что я делаю в моем цикле испытаний, чтобы создать новую функциональность? Кажется, это нарушает принцип «красный, зеленый, рефакторинг».

Должен ли я просто написать код, который обнаруживает, что lineSegments Intersect внутри функции CollidesWithLine и рефакторинг его после того, как он работает? Это сработало бы в этом случае, так как я могу получить доступ к данным из LineSegment , но что делать в случаях, когда такие данные являются частными?

Джошуа Харрис
источник

Ответы:

14

Просто закомментируйте свой тест и недавний код (или поместите в тайник), чтобы фактически повернуть время назад к началу цикла. Затем начните с LineSegment.Intersects(LineSegment)теста / кода / рефактора. Когда это будет сделано, раскомментируйте свой предыдущий тест / код (или извлеките из хранилища) и продолжайте цикл.

Хавьер
источник
Чем это отличается, тогда просто игнорируя это и возвращаясь к нему позже?
Джошуа Харрис
1
только мелкие детали: в отчетах нет дополнительного теста «игнорируй меня», и если вы используете тайники, код неотличим от «чистого» случая.
Хавьер
Что такое заначка? это как контроль версий?
Джошуа Харрис
1
некоторые VCS реализуют его как функцию (по крайней мере, Git и Fossil). Это позволяет вам удалить изменение, но сохранить его для повторного применения через некоторое время. Это не сложно сделать вручную, просто сохраните diff и вернитесь к последнему состоянию. Позже вы повторно применяете diff и продолжаете.
Хавьер
6

В цикле TDD:

На этапе «сделать тестовый проход» вы должны написать простейшую реализацию, которая сделает тестовый проход . Чтобы пройти тест, вы решили создать нового соавтора, который будет обрабатывать недостающую логику, потому что, возможно, было слишком много работы для того, чтобы поместить в ваш точечный класс тест на прохождение теста. Вот в чем проблема. Я полагаю, что тест, который вы пытались пройти, был слишком большим шагом . Поэтому я думаю, что проблема заключается в самом вашем тесте, вы должны удалить / прокомментировать этот тест и найти более простой тест, который позволит вам сделать шаг вперед, не вводя часть LineSegment.Intersects (LineSegment). Если у вас есть этот тест, вы можете затем рефакторингваш код (здесь вы будете применять принцип SRP), переместив эту новую логику в метод LineSegment.Intersects (LineSegment). Ваши тесты все равно пройдут, потому что вы не изменили своего поведения, а просто переместили некоторый код.

На вашем текущем дизайнерском решении

Но для меня у вас есть более серьезная проблема в дизайне: вы нарушаете принцип единой ответственности . Роль Точки ... быть точкой, вот и все. Нет смысла в том, чтобы быть точкой, это просто значение x и y. Точки являются типами значений . Это то же самое, что и для сегментов, сегменты представляют собой типы значений, состоящие из двух точек. Они могут содержать немного «сообразительности», например, чтобы вычислить их длину на основе положения их точек. Но это все.

Теперь решить, сталкиваются ли точка и отрезок, - это отдельная ответственность. И, безусловно, слишком много работы для точки или сегмента, чтобы справиться самостоятельно. Он не может принадлежать классу Point, потому что иначе Point будет знать о сегментах. И он не может принадлежать сегментам, потому что сегменты уже несут ответственность за заботу о точках внутри сегмента и, возможно, также за вычисление длины самого сегмента.

Таким образом, эта ответственность должна принадлежать другому классу, такому как, например, «PointSegmentCollisionDetector», который будет иметь такой метод:

bool AreInCollision (точка p, сегмент s)

И это то, что вы будете тестировать отдельно от точек и сегментов.

Приятно то, что теперь у вас может быть другая реализация вашего детектора столкновений. Так что было бы легко, например, сравнить свой игровой движок (я полагаю, вы пишете игру: p), переключив метод обнаружения столкновений во время выполнения. Или сделать некоторые визуальные проверки / эксперименты во время выполнения между различными стратегиями обнаружения столкновений.

На данный момент, внедряя эту логику в свой класс Point, вы блокируете вещи и возлагаете слишком большую ответственность на класс Point.

Надеюсь, это имеет смысл,

foobarcode
источник
Вы правы , что я пытался испытания слишком больших изменений , и я думаю , что вы правы насчет разделяющей , что из в класс столкновения, но это заставляет меня задать совершенно новый вопрос , который вы могли бы быть в состоянии помочь мне: Должна ли я использовать интерфейс, когда методы только похожи? ,
Джошуа Харрис
2

Самое простое, что можно сделать в режиме TDD, - это извлечь интерфейс для LineSegment и изменить параметр метода, чтобы он воспринимал интерфейс. Затем вы можете смоделировать входной отрезок и кодировать / тестировать метод Intersect независимо.

Дэн Лайонс
источник
1
Я знаю, что это метод TDD, который я слышу больше всего, но ILineSegment не имеет смысла. Одно дело связать внешний ресурс или что-то, что может принимать разные формы, но я не вижу ни одной причины, по которой я бы связывал какую-либо функциональность с чем-либо, кроме линейного сегмента.
Джошуа Харрис
0

С jUnit4 вы можете использовать @Ignoreаннотацию для тестов, которые вы хотите отложить.

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

bakoyaro
источник