Off By One ошибки и тестирование мутаций

В процессе написания тестера мутаций "Off By One" для моей любимой среды тестирования мутаций (NinjaTurtles) я написал следующий код, чтобы дать возможность проверить правильность моей реализации:

public int SumTo(int max)
{
    int sum = 0;
    for (var i = 1; i <= max; i++)
    {
        sum += i;
    }
    return sum;
}

теперь это кажется достаточно простым, и мне не пришло в голову, что возникнет проблема при попытке изменить все буквальные целочисленные константы в IL. В конце концов, их всего 3 (0, 1 и ++).

НЕПРАВИЛЬНО!

С первого запуска стало совершенно очевидно, что в данном конкретном случае это никогда не сработает. Почему? Поскольку изменение кода на

public int SumTo(int max)
{
    int sum = 0;
    for (var i = 0; i <= max; i++)
    {
        sum += i;
    }
    return sum;
}

только добавляет 0 (ноль) к сумме, и это, очевидно, не имеет никакого эффекта. Другая история, если это был многократный набор, но в данном случае это не так.

Теперь есть довольно простой алгоритм вычисления суммы целых чисел.

sum = max * (max + 1) / 2;

что я мог бы легко потерпеть неудачу при мутации, так как добавление или вычитание 1 из любой из констант приведет к ошибке. (учитывая, что max >= 0)

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

Итак, мой вопрос: Существуют ли какие-либо тривиальные или нетривиальные случаи, когда цикл, начинающийся с 0 или 1, может привести к сбою теста "изменение на единицу", которое не может быть исправлено (тестируемый код или test) аналогичным образом? (примеры, пожалуйста)

Примечание. Тесты на мутацию не проходят, если набор тестов проходит после применения мутации.

Обновление: пример чего-то менее тривиального, но что-то, что можно было бы отрефакторировать так, чтобы он не прошел, будет следующее

public int SumArray(int[] array)
{
    int sum = 0;
    for (var i = 0; i < array.Length; i++)
    {
        sum += array[i];
    }

    return sum;
}

Мутационное тестирование этого кода завершится ошибкой при изменении var i=0 на var i=1, если введенный вами тестовый ввод был new[] {0,1,2,3,4,5,6,7,8,9}. Однако измените тестовый вход на new[] {9,8,7,6,5,4,3,2,1,0}, и проверка мутации завершится неудачно. Итак, успешный рефакторинг - это доказательство тестирования.


person pms1969    schedule 30.04.2012    source источник


Ответы (4)


Один естественный случай «неудачного теста на мутацию» - это алгоритм транспонирования матрицы. Чтобы сделать его более подходящим для одиночного цикла for, добавьте к этой задаче некоторые ограничения: пусть матрица не квадратная и требуется транспонирование на месте. Эти ограничения делают одномерный массив наиболее подходящим местом для хранения матрицы, и для его обработки можно использовать цикл for (обычно начинающийся с индекса '1'). Если вы начнете его с индекса «0», ничего не изменится, потому что верхний левый элемент матрицы всегда транспонируется сам на себя.

Пример такого кода см. В разделе ответ на другой вопрос (не на C #, извините).

Здесь тест «изменение на единицу» не проходит, рефакторинг теста его не меняет. Я не знаю, можно ли отредактировать сам код, чтобы этого избежать. Теоретически это возможно, но должно быть слишком сложно.


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

Итерируйте по массиву положительных целых чисел в порядке возрастания индексов, для каждого индекса вычислите его пару как i + i % a[i], и, если он не выходит за границы, поменяйте местами эти элементы:

for (var i = 1; i < a.Length; i++)
{
    var j = i + i % a[i];
    if (j < a.Length)
        Swap(a[i], a[j]);
}

Здесь [0] снова является «неподвижным», рефакторинг теста не меняет этого, а рефакторинг самого кода практически невозможен.


Еще один «содержательный» пример. Давайте реализуем неявную двоичную кучу. Обычно он помещается в некоторый массив, начиная с индекса «1» (это упрощает многие вычисления двоичной кучи по сравнению с запуском с индекса «0»). Теперь реализуйте метод копирования для этой кучи. Проблема «по одному» в этом методе копирования не обнаруживается, потому что нулевой индекс не используется, а C # инициализирует нулем все массивы. Это похоже на суммирование массивов OP, но не может быть подвергнуто рефакторингу.

Строго говоря, вы можете провести рефакторинг всего класса и начать все с «0». Но изменение только метода «копирования» или теста не предотвращает «мутацию за один» провал теста. Класс двоичной кучи можно рассматривать просто как побуждение скопировать массив с неиспользуемым первым элементом.

int[] dst = new int[src.Length];
for (var i = 1; i < src.Length; i++)
{
    dst[i] = src[i];
}
person Evgeny Kluev    schedule 03.05.2012
comment
Итак, значимая задача кода, когда начало с индекса 0 или 1 имеет тот же эффект, потому что, как и в случае суммирования, операция с индексом 0 не имеет никакого эффекта. И в этом случае, если не прибегать к внешней библиотеке для матричных вычислений, рефакторинга не происходит - если вы считаете само собой разумеющимся использование одномерного массива. Интересно - раздумываю над этим. - person David M; 03.05.2012
comment
@evgeny - Я только что закончил разбираться в твоем исходном посте. Но оно того стоило. Я взял C ++, сделал его C # и возился. Изначально я был полностью озадачен тем, как провести рефакторинг, чтобы добиться результата, который можно было бы доказать с помощью мутационного тестирования. В конце концов, у меня был момент с лампочкой, и это применимо и к этому примеру. Применение некоторой неизменности к методу здесь творит чудеса. Поэтому вместо void верните char[], и у вас будет некоторый доказуемый код и, возможно, метод, который в высшей степени лучше для опыта. - person pms1969; 04.05.2012
comment
@ pms1969, верно, тут помогает неизменность. Но это снимает ограничение на алгоритмы. Я не думаю, что можно рассматривать модифицированный таким образом алгоритм как тот же алгоритм. Для транспонирования матриц существует очень простой алгоритм не на месте, но все алгоритмы на месте довольно продвинуты. Кстати, я просто добавил еще один пример, который уже неизменен. - person Evgeny Kluev; 04.05.2012
comment
И я бы согласился с комментарием о ограничении на месте, но, возможно, нам нужно спросить себя, почему это ограничение существует при написании нашего собственного кода? Более простой код легче поддерживать и реорганизовывать. И тот факт, что тестирование на мутации не может подтвердить алгоритм на месте, может заставить вас пересмотреть свой дизайн (королевский вы там). Единственный раз, когда я могу думать о том, где может действовать ограничение, - это работа с огромными наборами данных, которые могут взорвать вашу память при копировании. - person pms1969; 04.05.2012
comment
В ПОРЯДКЕ. Добавил код для самой копии. Надеюсь, писать реализацию всего класса не нужно. Я не использовал Array.Copy, потому что в этом случае нечего тестировать. Что касается ограничения на месте, единственное, что имеет значение для транспонирования матрицы, - это огромный набор данных. Для других алгоритмов ограничение на месте может иметь другие преимущества. Лучше изменить 10% набора данных, чем скопировать все. (По крайней мере, это верно для императивных языков; функциональные языки используют связанные структуры, где неизменяемость более удобна). - person Evgeny Kluev; 04.05.2012
comment
Спасибо за это Евгению. Думаю, вам удалось продемонстрировать несколько нетривиальных примеров, чего мы с Полом действительно интересовали. Думаю, вы заработали бонус. :) - person David M; 09.05.2012
comment
абсолютно. Очень признателен. - person pms1969; 11.05.2012

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

Ваш вопрос действительно сводится к следующему: существует ли реальная жизненная ситуация, когда мы заботимся о том, включен ли элемент 0 или исключен из операции цикла, и для которой мы не можем написать тест вокруг этого конкретного аспекта < / strong>? Мой инстинкт - сказать нет.

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

person David M    schedule 30.04.2012
comment
Точно. Моего инстинкта тоже нет. Нетривиальный пример всегда должен потерпеть неудачу, если вы измените начальную константу цикла for, и я подозреваю, что мой надуманный пример - лишь один из немногих реальных случаев, которые не потерпят неудачу при его изменении. Думаю, будет интересен даже банальный пример, который невозможно отрефакторить. Я изменю вопрос. - person pms1969; 30.04.2012

Да, их много, если я понял ваш вопрос.

Один, похожий на ваш случай:

public int MultiplyTo(int max)
{
    int product = 1;
    for (var i = 1; i <= max; i++)
    {
        product *= i;
    }
    return product;
}

Здесь, если он начинается с 0, результатом будет 0, но если он начинается с 1, результат должен быть правильным. (Хотя разница между 1 и 2 здесь не заметна!).

person Justin    schedule 30.04.2012
comment
Я думаю, что OP после нетривиального примера, который не проходит тестирования мутации - этот проходит, потому что начало цикла i = 0 дает результат 0, а начало цикла i = 1 дает ненулевой результат. - person David M; 30.04.2012
comment
В этом больше смысла ... Итак, его вопрос на самом деле говорит: вот пример того, что я ищу, но есть ли другой случай, менее тривиальный? - person Justin; 30.04.2012
comment
ну, на самом деле, глядя на это, изменение var i = 1 на var i = 2 не удастся, поскольку умножение на 1 не влияет на ответ. Все еще довольно надуманный ответ. Есть ли здесь рефакторинг, который может сработать? - person pms1969; 30.04.2012
comment
Да, здесь может сработать рефакторинг. Сделайте метод рекурсивным. - person David M; 02.05.2012

Не совсем уверен, что именно вы ищете, но мне кажется, что если вы измените / мутируете начальное значение суммы с 0 на 1, вы должны провалить тест:

public int SumTo(int max) 
{ 
  int sum = 1; // Now we are off-by-one from the beginning!
  for (var i = 0; i <= max; i++) 
  { 
    sum += i; 
  } 
  return sum; 
}

Обновление на основе комментариев:

Цикл не потерпит неудачу только после мутации, когда инвариант цикла будет нарушен при обработке индекса 0 (или при его отсутствии). Большинство таких частных случаев можно реорганизовать вне цикла, но рассмотрите возможность суммирования 1 / x:

for (var i = 1; i <= max; i++) {
  sum += 1/i;
}

Это работает нормально, но если вы измените исходный пакет с 1 на 0, тест завершится неудачно, поскольку 1/0 - недопустимая операция.

person Attila    schedule 30.04.2012
comment
Да, будет, но меня здесь интересует цикл for. - person pms1969; 30.04.2012
comment
Насколько я понимаю ваш вопрос, если все операции в цикле действительны, то нет никакой разницы между началом с 0 или 1 (результат, очевидно, может быть другим, но инвариант (с измененными границами) останется). Вы можете сделать i <= max+0? Если это нормально, преобразование его в i<=max+1 может быть недопустимым (например, переиндексирование массива) - person Attila; 30.04.2012
comment
поэтому константы - это то, что видоизменяется, и вы правы, чрезмерное индексирование вполне может вызвать сбой (но я не думаю, что мы можем гарантировать это с помощью примера суммы. Я отредактирую сообщение, чтобы привести пример что-то менее тривиальное и не сработало бы при определенных входных данных.Сам тест можно было бы отредактировать так, чтобы входные данные затем не прошли бы тест, и, таким образом, было бы достигнуто хорошее покрытие / тестирование. - person pms1969; 30.04.2012
comment
Другая возможность заключается в том, что цикл требует, чтобы вы начинали с 0 (например, шаг 0 - это своего рода инициализация), тогда мутация приведет к сбою теста - person Attila; 30.04.2012
comment
который затем можно было бы реорганизовать, чтобы выполнить инициализацию вне цикла. Разве вы можете представить себе пример того, что не может? - person pms1969; 30.04.2012
comment
Если ваш инвариант цикла таков, что он выполняется как при запуске с 0, так и при запуске с 1, нет возможности потерпеть неудачу с вашей постепенной мутацией. Если цикл таков, что он выполняется только в том случае, если вы начинаете с 0 (эквивалентно: только если вы начинаете форму 1), то ваша мутация не пройдет проверку. Наличие особого случая при значении 0, который вы должны (эквивалентно: не должны) обрабатывать, приведет к тому, что вы ищете. Другой особый случай: сумма обратных чисел: 1/0 не должна вычисляться, но 1/i i = [1, ... n] в порядке. - person Attila; 30.04.2012
comment
позвольте нам продолжить обсуждение в чате - person pms1969; 30.04.2012