Борьба с циклическими зависимостями в модульных тестах

24

Я пытаюсь попрактиковаться в TDD, используя его для разработки простого типа Bit Vector. Я использую Swift, но это не зависит от языка.

My BitVector- это объект, structкоторый хранит один UInt64и представляет API, который позволяет вам рассматривать его как коллекцию. Детали не имеют большого значения, но все довольно просто. Старшие 57 бит - это биты хранения, а младшие 6 бит - это биты «подсчета», которые сообщают вам, сколько битов хранения на самом деле хранят содержащиеся значения.

Пока у меня есть несколько очень простых возможностей:

  1. Инициализатор, который создает пустые битовые векторы
  2. countСвойство типаInt
  3. isEmptyСвойство типаBool
  4. Оператор равенства ( ==). NB: это оператор равенства значений, похожий на Object.equals()Java, а не оператор равенства ссылок, как ==в Java.

Я сталкиваюсь с кучей циклических зависимостей:

  1. Модульный тест, который проверяет мой инициализатор, должен проверить, что он создан заново BitVector. Это можно сделать одним из 3 способов:

    1. Проверьте bv.count == 0
    2. Проверьте bv.isEmpty == true
    3. Проверь это bv == knownEmptyBitVector

    Метод 1 основан на countметоде 2 isEmpty(который сам по себе полагается count, поэтому нет смысла его использовать), метод 3 использует ==. В любом случае, я не могу проверить свой инициализатор изолированно.

  2. Тест countдолжен работать на чем-то, что неизбежно проверяет мой инициализатор (ы)

  3. Реализация isEmptyопирается наcount

  4. Реализация ==опирается на count.

Я смог частично решить эту проблему, представив частный API, который конструирует BitVectorиз существующего битового шаблона (как UInt64). Это позволило мне инициализировать значения без тестирования каких-либо других инициализаторов, чтобы я мог «загрузиться».

Чтобы мои модульные тесты действительно были модульными тестами, я обнаружил, что выполняю кучу хаков, которые существенно усложняют мой продукт и тестовый код.

Как именно вы справляетесь с такими проблемами?

Александр - Восстановить Монику
источник
20
Вы придерживаетесь слишком узкого взгляда на термин «единица». BitVectorидеально подходит для модульного тестирования и сразу решает ваши проблемы, в которых публичные члены BitVectorнуждаются друг в друге для проведения значимых тестов.
Барт ван Инген Шенау
Вы знаете слишком много деталей реализации заранее. Действительно ли испытательно развитие вашей инициативе ?
Herby
@ Херби Нет, поэтому я тренируюсь. Хотя это кажется действительно недостижимым стандартом. Я не думаю, что когда-либо программировал что-либо без довольно ясного умственного приближения того, что повлечет за собой реализация.
Александр - Восстановить Монику
@ Александр Вам следует постараться расслабиться, иначе это будет сначала тест, но не тестирование. Просто скажи расплывчато: «Я сделаю небольшой вектор с одним 64-битным int в качестве резервного хранилища», и все; с этого момента делайте TDD красно-зеленый-рефакторинг один за другим. Детали реализации, а также API должны возникать при попытке запустить тесты (первый) и при написании этих тестов в первую очередь (второй).
Herby

Ответы:

66

Вы слишком беспокоитесь о деталях реализации.

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

  • Это имеет только что инициализированный объект count == 0.
  • Что недавно инициализированный объект имеет isEmpty == true
  • То, что вновь инициализированный объект равен известному пустому объекту.

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

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

Филип Кендалл
источник
6
@ Александр Вы говорите как человек, нуждающийся в четком определении модульного тестирования. Лучшее, что я знаю, это Майкл
Фезерс
14
@ Александр, вы рассматриваете каждый метод как независимый тестируемый фрагмент кода. Это источник ваших трудностей. Эти трудности исчезают, если вы тестируете объект в целом, не пытаясь разделить его на более мелкие части. Зависимости между объектами несопоставимы с зависимостями между методами.
Амон
9
@ Александр "кусок кода" - произвольное измерение. Просто инициализируя переменную, вы используете много «кусочков кода». Важно то, что вы тестируете целостную поведенческую единицу, определенную вами .
Муравей Р
9
«Из того, что я прочитал, у меня сложилось впечатление, что, если вы нарушаете только часть кода, только модульные тесты, имеющие непосредственное отношение к этому коду, должны провалиться». Это, кажется, очень сложное правило для подражания. (например, если вы пишете векторный класс, и вы делаете ошибку в методе index, у вас, вероятно, будет множество ошибок во всем коде, который использует этот векторный класс)
jhominal
4
@Alexander Кроме того, посмотрите на шаблон "Arrange, Act, Assert" для тестов. По сути, вы устанавливаете объект в том состоянии, в котором он должен быть (Arrange), вызываете метод, который вы фактически тестируете (Act), а затем проверяете, что его состояние изменилось в соответствии с вашими ожиданиями. (Утверждай). То, что вы настроили в Arrange, будет «предварительным условием» для теста.
GalacticCowboy
5

Как именно вы справляетесь с такими проблемами?

Вы пересматриваете свои мысли о том, что такое «модульный тест».

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

На практике это часто выглядит так

// GIVEN
obj = new Object(...)

// THEN
assert object.read(...)

или

// GIVEN
obj = new Object(...)

// WHEN
object.change(...)

// THEN
assert object.read(...)

Терминология «модульного теста» - ну, у нее давняя история не очень хорошего качества.

Я называю их модульными тестами, но они не очень хорошо соответствуют принятому определению модульных тестов - Кент Бек, Test Driven Development by Example

Кент написал первую версию SUnit в 1994 году , порт для JUnit был в 1998 году, первый проект книги TDD был в начале 2002 года. Путаница имела много времени для распространения.

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

Основной вариант использования этих тестов заключается в том, что они выполняются программистом между изменениями в ее собственном исходном коде. Если вы выполняете красно-зеленый протокол рефакторинга, неожиданный КРАСНЫЙ всегда указывает на ошибку в вашем последнем редактировании; вы отменяете это изменение, проверяете, что тесты ЗЕЛЕНЫЕ, и попробуйте снова. Нет большого преимущества в попытках инвестировать в дизайн, где каждая возможная ошибка обнаруживается только одним тестом.

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

VoiceOfUnreason
источник
1

В целом (даже если вы не используете TDD) вы должны стараться писать как можно больше тестов, делая вид, что не знаете, как это реализовано.

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

То, как граф вызовов выглядит под тестами, не имеет значения, если сами тесты разумны и хорошо поддерживаются.

Я думаю, что ваша проблема в вашем понимании TDD.

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

Конечно, в принципе было бы лучше, если бы все тесты были ортогональными и независимыми друг от друга. Но это не касается двух других персонажей TDD, и это определенно не является строгим или даже обязательно реалистичным жестким требованием ваших тестов. По сути: не отбрасывайте свои здравые чувства по поводу качества кода, чтобы попытаться выполнить требование, о котором никто не просит вас.

Тим Сегин
источник