TDD - написание тестов для метода, который выполняет итерации/работает с коллекциями

Являясь новичком в TDD, я начинаю писать блок-тесты, посвященные коллекциям. Например, на данный момент я пытаюсь придумать некоторые тестовые сценарии, чтобы по существу протестировать следующий метод

int Find(List<T> list, Predicate<T> predicate);

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

  • Когда list не содержит элементов - возвращает -1
  • Когда list содержит 1 элемент, который соответствует predicate - возвращает 0
  • Когда list содержит 1 элемент, который не соответствует predicate - возвращает -1
  • Когда list содержит 2 элемента, оба из которых соответствуют predicate - return 0
  • Когда list содержит 2 элемента, первый из которых соответствует predicate - возвращает 0
  • и т.д...

Как вы можете видеть, однако эти тестовые примеры являются многочисленными и не удовлетворительно проверяют фактическое поведение, которое я действительно хочу. Математик во мне хочет сделать какой-то TDD-по-индукции

  • Когда list не содержит элементов - возвращает -1
  • Когда list содержит N элементов, вызовите predicate в первом элементе, а затем рекурсивно вызовите Find для остальных элементов N-1

Однако это вводит ненужную рекурсию. Какие типы тестов я должен искать для записи в TDD для вышеуказанного метода?


В отличие от метода, который я пытаюсь проверить на самом деле, просто Find, просто для конкретной коллекции и предиката (который я могу самостоятельно писать для тестовых примеров). Разумеется, для меня должен быть способ избежать необходимости писать какие-либо из вышеперечисленных тестовых примеров, а просто просто проверить, что метод вызывает некоторую другую реализацию Find (например, FindIndex) с правильными аргументами?

Обратите внимание, что в любом случае мне все же хотелось бы знать, как я мог unit test Find (или другой метод вроде этого), даже если окажется, что в этом случае мне не нужно.

Ответы

Ответ 1

Если find() работает, то он должен вернуть индекс первого элемента, который соответствует предикату, правильно?

Таким образом, вам понадобится тест для пустого списка, а один - для случая несоответствующих элементов, а другой - для соответствующего элемента. Я бы нашел это достаточно. В ходе TDDing find() я мог бы написать специальный случай с первым элементом, который я мог бы легко подделать. Я бы, наверное, написал:

emptyListReturnsMinusOne()
singlePassingElementReturnsZero()
noPassingElementsReturnsMinusOne()
PassingElementMidlistReturnsItsIndex()

И ожидайте, что последовательность приведет к моей правильной реализации.

Ответ 2

Прекратить тестирование, когда страх заменяется скукой - Кент Бек

В этом случае, какова вероятность того, что данный проходной тест для

  • "Когда список содержит 2 элемента, оба из которых соответствуют предикату - возвращают 0"

следующий тест не будет выполнен?

  • "Когда список содержит 5 элементов, оба из которых соответствуют предикату - return 0"

Я бы написал первое, потому что боюсь, что поведение не работает для нескольких элементов. Однако, как только 2 работает, писать еще один для 5 - это просто скука (если в производственном коде нет жестко зафиксированного предположения о 2, которое должно было быть реорганизовано. Даже если это не так, я бы просто изменил существующий тест на иметь 5 вместо 2 и заставить его работать в общем случае).

Итак, пишите тесты для разных вещей. В этом случае список с (ноль, один, много) элементов и (содержит/не содержит) операнд

Ответ 3

Исходя из вашего требования к методу Find, вот что я хотел бы проверить:

  • list - null - throws ArgumentNullException или возвращает -1
  • list не содержит элементов - возвращает -1
  • predicate - null - throws ArgumentNullException или возвращает -1
  • list содержит один элемент, который не соответствует predicate - возвращает -1
  • list содержит один элемент, который соответствует predicate - возвращает 0
  • list содержит несколько элементов, но ни один элемент не соответствует predicate - возвращает -1
  • list содержит несколько элементов, которые соответствуют predicate - возвращает индекс первого совпадения

В принципе, вы сначала должны проверить для конечных случаев - нулевые аргументы, пустой список. После этого проверяется один предмет. Наконец, соответствие теста и несоответствие для нескольких элементов.

Для аргументов null вы можете либо сбрасывать исключение, либо возвращать -1, в зависимости от ваших предпочтений.

Ответ 4

Не меняйте список, измените предикаты .

Подумайте, как будет вызван метод. Когда кто-то вызывает метод Find, у них уже есть список и нужно думать о предикатах. Поэтому подумайте о хороших примерах, демонстрирующих поведение Find:

Пример: Использование того же списка 3, 4 для всех тестовых кодов позволяет легко понять:

  • Предикат < 5 соответствует обоим числам (возвращает 1)
  • Предикат == 3 соответствует 3 (возвращает 0)
  • Предикат == 0 не соответствует ни одному (возвращает -1)

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

Ответ 5

Чтобы попытаться ответить на ваш вопрос: у меня нет опыта работы с носорогами Rhino, но я считаю, что у него должно быть что-то похожее на FakeItEasy (?):

var finder = A.Fake<IMyFindInterface>();

// ... insert code to call IMyFindInterface.Find(whatever) here

A.CallTo(() => finder.find(A<List>.That.Matches(
                  x => x.someProperty == someValue))).MustHaveHappened();

Поместив реализацию Find() за интерфейс, а затем передав метод, который будет использовать этот интерфейс подделкой, вы можете проверить, что метод вызывается с определенными параметрами. (Функция MustHaveHappended() приведет к сбою теста, если ожидаемый вызов не завершен).

Поскольку вы знаете, что реальная реализация IMyFindInterface просто передает вызов на реализацию, которую вы уже доверяете, это должен быть достаточно хороший тест, чтобы проверить, что тестируемый вами код правильно называет Find-реализацию.

Эта же процедура может использоваться всякий раз, когда вы просто хотите убедиться, что ваш код (тестируемое устройство) вызывает некоторый компонент, которому вы уже доверяете правильным образом, абстрагируясь от самого этого компонента - именно то, что мы хотим, когда модульное тестирование,