Развитие навыков тестирования и отладки программ

образовательный процесс
планирование занятия
программирование
Алгоритм составлен, программа написана и … она не запускается. Либо запускается, но даёт совсем не те результаты, что ожидались. Как “починить” код программы, как проверить, для всех ли данных программа даёт правильный ответ?
Дата публикации

10 января 2024 г.

Уделяется ли отладке и тестированию программ достаточно времени в книгах по программированию? В тех книгах по Python, что попадались мне, внимание заострялось на синтаксисе, алгоритмических конструкциях и особенностях использования специфических возможностей языка. Отладка готовой программы уходит на второй план, когда в книге даётся правильное решение задачи. Набирай его и получишь результат. Но нет гарантий, что учащийся перепечатает исходный код верно, не допустит опечатку. Пропуск одного символа может привести к синтаксической ошибке и не работающему примеру. Возможны следующие исходы: программа набирается заново и запускается как если бы предыдущий вариант не существовал вовсе, начинается долгая, построчная сверка кода из учебника (инструкции, книги) с набранным текстом, или ученик сдаётся и ждёт помощи преподавателя. Вывод - стоит уделать время явному обучению отлаживать и тестировать свой код.

Чтение и написание кода

Важный этап обучения программированию - умение читать код. В методической литературе предлагается следующая последовательность обучения1:

Семантика кода Шаблоны кода
Чтение 1. чтение кода и предсказание результата его выполнения 3. распознавание полезных шаблонов и областей их применения
Написание 2. запись кода с корректным синтаксисом 4. использование шаблонов для решения промежуточных задач

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

Тестирование

Тестирование необходимо для того, чтобы определить, выполняется ли программа так, как задумывалось и верен ли результат её выполнения. В контексте обучения программированию, тестирование имеет два значения: проверка программы учителем и самостоятельное тестирование программы учащимися во время разработки.

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

Тесты для задач могут быть как ручными, так и автоматизированными. Я склоняюсь к использованию последних. Это экономит время на проверку. Мне нравится задачник идущий в комплекте с PascalABC. Удобно, когда в среду разработки встраиваются такие инструменты.

Тесты составленные учащимися, покрывают, в среднем, только 13,6% случаев3. Более 90% тестовых кейсов, придуманных учащимися, схожи по смыслу. Начинающие программисты составляют тесты которые подтверждают правильность работы кода, но не помогают найти ошибки.

Примеры заданий

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

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

Ещё один вариант упражнения нашёл в книге по олимпиадному программированию. Две группы учащихся решают задачу. После этого обмениваются решениями. Задача каждой группы - придумать тесты, которые сломают программу другой группы (естественно входные данные должны подходить по условию задачи). Добавляется элемент соревновательности, который не всегда подходит для начинающих программистов. Но для занимающихся олимпиадным программированием будет в самый раз.

Общие советы и рекомендации

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

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

Далее приведу общие рекомендации по тестированию программ, которые не привязаны к конкретному языку программированию5. В примерах буду использовать Python.

Алгоритмы с ветвлением

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

Если, к примеру, от значения переменной x зависит значение выражения g, то необходимо подобрать входное значение так, чтобы проверить вычисление всех выражений:

x = int(input("x: "))
if x < 0:
    g = abs(x)
else:
    if x >= 5:
        g = x ** 2
    else:
        g = x ** 0.5
print(g)

Опять полезно напомнить учащимся о пограничных случаях. Что будет если ввести значения x на границе условия?

Алгоритмы с повторением

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

Рассмотрим на примере. Необходимо найти сумму элементов списка в Python. Решение может быть следующим:

numbers = [1, 3, 5]

acc = 0 # аккумулятор слагаемых
for num in numbers:
    acc = acc + num

print(acc) # результат - 9

Этот код сработает и для пустого списка. Но если в условии задачи требуется вывести -1 для пустого списка, предыдущее решение не подходит и требуются исправления:

numbers = []

# проверка размера списка
if len(numbers) == 0:
    acc = -1
else:
    acc = 0
    for num in numbers:
        acc = acc + num

print(acc) # результат равен -1

Тестирование процедур и функций

Вспомогательный алгоритм может выполнять следующие действия:

  • возвращать значение;
  • изменять содержимое переменной, переданной по ссылке (функция с побочными эффектами);
  • выводить результат на экран или в файл.

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

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

Рассмотрим функцию вычисления квадрата числа:

def sqr(x):
    return x * x.

Для 4, 10, 12 и других положительных чисел проверку можно считать эквивалентной. Это же относится и к отрицательным числам. Пограничным значением будет 0 - значение в котором один класс эквивалентности переходит в другой. Чтобы протестировать функцию sqr() достаточно трёх тестов: с положительным, отрицательным числом и нулём.

Отладка

Дебагинг, или отладка необходимы для исправления найденных на этапе тестирования ошибок.

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

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

Traceback (most recent call last):
File "d:\Work\PYTHON\python-intro\14debug\sam02.py", line 18, in <module>
 answ = fun(num1)
        ^^^
NameError: name 'fun' is not defined. Did you mean: 'func'?

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

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

import friendly
friendly.install(lang="ru")

Примеры заданий

Успешному поиску ошибок в коде способствуют:

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

Для развития навыка отладки программ, учащимся можно предложить следующие задания:

Предсказать результат работы программы: что произойдёт после запуска? какое значение будет выведено на экран? Чтобы усложнить задание, код программы сопровождаем синтаксическими и/или семантическими ошибками.

Пример. Каким будет результат работы следующих фрагментов кода:

x:=1;
while x < 10 do
begin
  x:=x+3;
  writeln(x);
end;

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

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

Пример. Запишите в таблицу значения переменных и результат проверки условия цикла на каждом шаге.

var s, i: real;
begin
s:=0;
i:=1;
while i < 6 do
  begin
   s : = s + i * i;
   i := i + 2;
  end;
writeln(s);
end.
шаг i i<6 s
1
2
3

Поиск и исправление ошибок в предоставленных исходных кодах: сложность задания будет обусловлена количеством ошибок; отдельное внимание необходимо уделить поиску семантических ошибок.

Пример. Найдите ошибки в следующей программе:

program lab1;
var num, c1, c2: integer;
begin
  readln(num);
  for i:=1 to num do
  begin
      if i mod 2 = 0 then
          c1 := c1 + 1;
      else
          c2 := c2 + 1;
      i:=i+1;
  end;    
  writeln('c1=', c1, 'c2=, c2);
end.


Некоторые рекомендации из это статьи мне уже удалось применить на занятиях. В курсе Python одна из лабораторных полностью посвящена типовых ошибкам и отладке. Хотя теперь понимаю, что нужно было уделить время тестированию и отладке на каждом занятии.

Сноски

  1. Ссылки на источники и полезная информация взяты с сайта Teaching Tech Together. Информация конкретно об этой методике описана в публикации: Benjamin Xie, Dastyni Loksa, Greg L. Nelson, Matthew J. Davidson, Dongsheng Dong, Harrison Kwik, Alex Hui Tan, Leanne Hwa, Min Li, and Amy J. Ko: “A theory of instruction for introductory programming skills”. Computer Science Education, 29(2-3), 1 2019, doi:10.1080/08993408.2019.1565235.↩︎

  2. В промышленном программировании такой подход называется Test Driven Development - разработка через тестирование. Сначала пишется набор автоматизированных тестов, а затем пишется код. Со временем код проходит всё больше тестов. Задача считается решённой, когда код проходит все тесты.↩︎

  3. Stephen H. Edwards and Zalia Shams: “Do Student Programmers All Tend to Write the Same Software Tests?”. In 2014 Conference on Innovation and Technology in Computer Science Education (ITiCSE’14), 2014, doi:10.1145/2591708.2591757↩︎

  4. Идею задачи взял с этого сайта. Там же есть интерактивная демонстрация, которая по введённым тестовым данным показывает процент покрытия кода тестами. Выбить даже 50% весьма непросто.↩︎

  5. Рекомендации взяты из интерактивного учебника Foundations of Python Programming.↩︎

  6. Laurie Murphy, Gary Lewandowski, Renée McCauley, Beth Simon, Lynda Thomas, and Carol Zander: “Debugging: The Good, the Bad, and the Quirky - A Qualitative Analysis of Novices’ Strategies”. ACM SIGCSE Bulletin, 40(1), 2 2008, doi:10.1145/1352322.1352191. В этой же публикации есть список методов, которыми пользуются учащиеся для отладки собственных программ.↩︎