Как вы тестируете код с использованием графовых структур?

18

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

Хотя обычно 100% покрытия строк или ветвей достаточно, чтобы быть уверенным в том, что какой-то код работает, создается впечатление, что даже при 100% покрытии пути у вас все еще есть сомнения.

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


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


PPS- Это дополнительное постановление проблемы было первоначально опубликовано автором вопроса в комментарии ниже:

For all vertices N in forest F, for all vertices M, in F, such that if there are any walks between N and M they all must either use only edges labelled 'conflict' or 'requires'.

санки
источник
13
Точно так же, как вы тестируете модулем любой другой метод. Вы определяете все «интересные» тестовые случаи для каждого метода и пишете для них модульные тесты. В вашем случае вам придется создавать постоянные графы зависимостей для каждой из «интересных» структур графов.
Данк
@ Данк Мы продолжаем думать, что у нас есть все хитрые, и затем мы понимаем, что определенная структура вызывает проблемы, которые мы не рассматривали ранее. Тестирование каждого хитрости, о которой мы можем думать, это то, что мы делаем, и я надеюсь найти некоторые руководящие принципы / процедуры для генерации проблемных примеров, возможно, с использованием сводимости фундаментальных форм и т. Д.
Сани
6
Это проблема с любой формой тестирования. Все, что вы знаете, это то, что тесты, которые вы подумали о работе. Это не значит, что ваш sw не содержит ошибок только потому, что ваши тесты пройдены. У каждого проекта есть та же самая проблема. Я нахожусь на заключительной стадии доставки моего текущего проекта, чтобы мы могли начать производство. Типы ошибок, с которыми мы сталкиваемся сейчас, имеют тенденцию быть довольно неясными. Например, когда аппаратное обеспечение все еще работает в соответствии со спецификацией, но едва и когда в сочетании с другим оборудованием одновременно возникает одна и та же проблема, возникают проблемы; но только иногда :( SW хорошо проверен, но мы не думали обо всем
Dunk
То, что вы описываете, больше похоже на интеграционное тестирование, а не на юнит-тестирование. Модульные тесты позволят убедиться, что метод может найти круги на графике. Другие модульные тесты позволят убедиться, что конкретный круг определенного графа обрабатывается тестируемым классом.
SpaceTrucker
Обнаружение циклов - это хорошо освещенная тема (см. Кнут, а также некоторые ответы ниже), и решения не включают в себя большое количество особых случаев, поэтому вам следует сначала определить, что делает вашу проблему такой. Это из-за противоречий, которые вы упоминаете? Если это так, нам нужно больше информации о них. Если это результат выбора реализации, возможно, вам придется провести рефакторинг, возможно, в значительной степени. По сути, это проблема проектирования, которую вам придется продумать, TDD - это неправильный подход, который может привести вас в лабиринт до тупика.
sdenham

Ответы:

5

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

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

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

Наконец, научитесь принимать, что каждый тест может только доказать наличие ошибок, но не отсутствие ошибок.

Док Браун
источник
5

1. Генерация рандомизированных тестов

Напишите алгоритм, который генерирует графики, сгенерируйте несколько сотен (или более) случайных графиков и бросьте каждый на ваш алгоритм.

Сохраните случайное начальное число графиков, которые вызывают интересные сбои, и добавьте их в качестве модульных тестов.

2. Жесткий код хитрых частей

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

3. Создайте исчерпывающий список

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

Маке
источник
Опрашивающий написал, что они не могут сказать, рассматривали ли они все случаи, что означает, что у них нет способа перечислить их. Пока они не понимают проблемную область достаточно хорошо, чтобы сделать это, вопрос тестирования является спорным вопросом.
sdenham
@sdenham Как вы собираетесь перечислять то, что буквально имеет бесконечное число возможных допустимых комбинаций? Я надеялся найти что-то вроде «это самые хитрые графовые структуры, которые часто выявляют ошибки в вашей реализации». Я достаточно хорошо понимаю домен, поскольку он прост: For all vertices N in forest F, for all vertices M, in F, such that if there are any walks between N and M they all must either use only edges labelled 'conflict' or 'requires'.домен не является проблемой.
Сани
@ArtB: Спасибо за разъяснение проблемы. Как вы сказали, что между любыми двумя вершинами не более одного ребра, и, очевидно, вы исключаете пути с циклами (или, по крайней мере, более одного прохода вокруг любого цикла), то, по крайней мере, мы знаем, что буквально не существует бесконечного числа возможные допустимые комбинации, что является прогрессом. Обратите внимание, что знание того, как перечислить все возможности, - это не то же самое, что сказать, что вы должны это сделать, поскольку это может послужить отправной точкой для аргументации в пользу правильности, которая, в свою очередь, может направлять тестирование. Я буду больше об этом думать ...
Сденхам
@ArtB: Вы должны изменить вопрос, включив в него обновление формулировки проблемы, которую вы дали здесь. Кроме того, это может помочь утверждать, что это направленные ребра (если это так) и что цикл будет считаться ошибкой в ​​графе, а не просто ситуация, которую алгоритм должен обработать.
Сденхэм
4

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

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

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

foreach nodes as node
    foreach nodes as tmp
        tmp.status = unmarked

    tovisit = []
    tovisit.push(node)
    node.status = required

    while |tovisit| > 0 do
        next = tovisit.pop()
        foreach next.requires as requirement
            if requirement.status = unmarked
                tovisit.push(requirement)
                requirement.status = required
            else if requirement.status = blacklisted
                return false
        foreach next.collides as collision
            if collision.status = unmarked
                requirement.status = blacklisted
            else if requirement.status = required
                return false
return true

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

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

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

РЕДАКТИРОВАТЬ: То, что этот алгоритм не доказывает, ваш график свободен от циклов. Направленные ациклические графы - это хорошо изученная тема, поэтому поиск готового алгоритма для доказательства этого свойства также должен быть легким.

Как видите, нет необходимости изобретать велосипед вообще.

Ext3h
источник
3

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

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

BobDalgleish
источник
Хотя это хороший подход к тестированию, он не решает главной проблемы вопроса: как обеспечить охват всех случаев. Это, я считаю, потребует дополнительного анализа и, возможно, рефакторинга - см. Мой вопрос выше.
Сденхам
2

Когда дело доходит до такого сложного для тестирования алгоритма, я бы выбрал TDD, где вы строите алгоритм на основе тестов,

Короче говоря,

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

и повторите цикл,

В этой конкретной ситуации,

  1. Первый тест будет, один узел графа, где алгоритм не должен возвращать циклов
  2. Вторым будет трехузловой граф без цикла, где алгоритм не должен возвращать циклов
  3. Далее следует использовать граф с тремя узлами с циклом, где алгоритм не должен возвращать циклов
  4. Теперь вы можете проверить его на более сложный цикл в зависимости от возможностей

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

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

Низко летящий пеликан
источник
2

Мое понимание проблемы, как первоначально заявлено, а затем дополнено комментариями в ответе Маке, включает следующее: 1) оба типа ребер (зависимости и конфликты) направлены; 2) если два узла соединены одним ребром, они не должны быть соединены другим, даже если он другого типа или наоборот; 3) если путь между двумя узлами можно построить путем смешивания ребер разных типов, то это скорее ошибка, чем игнорируемое обстоятельство; 4) Если существует путь между двумя узлами, использующими ребра одного типа, тогда между ними не может быть другого пути, использующего ребра другого типа; 5) циклы одного типа ребер или смешанных типов ребер недопустимы (из предположения приложения я не уверен, что циклы только конфликта являются ошибкой, но это условие может быть удалено, если нет).

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

Здесь на самом деле можно рассмотреть три графика: два исключительно одного типа ребер и смешанный граф, образованный объединением одного из каждого из двух типов. Вы можете использовать это, чтобы систематически генерировать все графики вплоть до некоторого количества узлов. Сначала сгенерируйте все возможные графы из N узлов, имеющих не более одного ребра между любыми двумя упорядоченными парами узлов (упорядоченные пары, потому что это ориентированные графы.) Теперь возьмем все возможные пары этих графов, один из которых представляет зависимости, а другой представляет конфликты, и сформировать объединение каждой пары.

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

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

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

Если пути смешанных ребер можно просто игнорировать, а не ошибки (условие 3), достаточно рассмотреть графы зависимостей и конфликтов независимо и проверить, что если узел доступен в одном, то нет в другом.

Если вы помните пути, найденные при проверке графов N-1 узлов, вы можете использовать их в качестве отправной точки для генерации и оценки графов из N узлов.

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

Ключ к написанию оракула, как это, состоит в том, чтобы сделать его как можно более простым, даже если это означает, что он неэффективен, так что вы можете установить доверие к нему (в идеале с помощью аргументов в пользу его правильности, подкрепленных тестированием).

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

sdenham
источник