3. Тестирование и обработка исклительных ситуаций

We use cookies. Read the Privacy and Cookie Policy

3. Тестирование и обработка исклительных ситуаций

3.1 Unit–тесты. Проанализировав, на что уходит время у большинства программистов, – можно обнаружить, что на написание кода в действительности тратится совсем небольшая часть. Какая-то часть уходит на понимание задачи, еще кусок на проектирование, а большую часть времени занимает отладка. Отладка – это процесс проверки программы на соответствие поставленной задаче, хотя чаще под этим определением понимают работу по нахождению ошибок в программе. Отладка – это «страшная вещь», и любой программист может рассказать о том, как на поиски какой-то ошибки ушел день или даже больше. Исправить ошибку можно довольно быстро, но самое сложное – найти её. А при исправлении ошибки всегда существует возможность добавить новую, которая проявится гораздо позднее.

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

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

Очевидно такая схема написания неудобна. Например, пусть отлаживаемая программа ищет пересечение двух прямоугольников. В процессе проверки нам придется неоднократно (для проверки различных частных случаев) вводить координаты различных прямоугольников (по восемь чисел на тест). Понятно, что необходимо как-то автоматизировать этот процесс. Один из способов – поместить тестовые данные в файл и считывать их оттуда при старте программы. Такой способ позволяет нам экономить на вводе тестовых данных, но этапа сравнения полученных решений с требуемыми при этом способе не избежать. Для решения этой задачи программисты на Ruby обычно используют специальную библиотеку TestUnit (подобного рода библиотеки есть в большинстве современных языков программирования, в языке Java, например, для этих целей используется пакет JUnit).

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

def number_decomposition(number)

numl, num2 = number/2, number/2

while numl > 1 and num2 < number if prime?(num1) and prime?(num2)

return numl, num2

end

     numl -= 1 num2 += 1 end

     return nil end

if __FILE__ == $0

     p number_decomposition(142)

end

Метод number_decomposition использует рассмотренный (стр. 10) нами ранее метод prime? (проверка числа на простоту).

Исходная программа была подготовлена к процессу тестирования. Метод number_decomposition возвращает полученный результат (вместо печати, который мы будем сравнивать с набором подготовленных шаблонов). Тело условного оператора в конце программы будет выполнено только в случае, если интерпретатору на выполнение будет передан данный файл с программой, при подключении этого файла командой require условие __FILE__ == $0 будет ложно.

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

Метод assert получает два аргумента (второй – необязательный): логическое выражение и текстовое сообщение. Если логическое выражение окажется ложно, то программа добавит 1 к числу неуспешных проверок и выведет сообщение о неудаче. Кроме этого, будет напечатано текстовое сообщение, переданное методу в качестве второго аргумента. Метод assert_equal получает три аргумента (третий – необязательный): ожидаемое значение, сравниваемое значение (как правило результат работы тестируемой функции) и сообщение, которое будет выведено при несовпадении первого и второго аргументов. Метод assert_nil проверяет, является ли аргумент объектом nil.

# подключение библиотеки тестирования require "test/unit"

# подключение файла с программой require "number_decomposition4test.rb"

class TestNumberDecomposition < Test::Unit::TestCase def setup

# набор начальных действий выполняемых

# перед запуском каждого тестового случая. end

# проверка работоспособности функции prime? def test_prime?

assert(false == prime?(1)) assert false == prime?(10)

assert_equal(true, prime?(2), "Ошибка при проверке числа 2") assert_equal true, prime?(7), "Ошибка при проверке числа 7" assert_equal(true, prime?(103)) end

def test_number_decomposition

assert_nil(number_decomposition(2)) assert_equal([5, 5], number_decomposition(10)) assert_equal [7, 11], number_decomposition(18) assert_equal [97, 103], number_decomposition(200) end end

Вы можете добавлять в класс вспомогательные методы и использовать их в тестовых случаях, но только методы, начинающиеся с test, будут запущены во время выполнения теста! Тесты вызываются в том порядке, в котором они представлены в программе. Если в тестирующем классе присутствуют методы с именами setup и (или) teardown, то они будут выполнены соответственно перед запуском всех тестов и после их завершения.

    Очень полезно писать тесты до начала программирования.

Посмотрим на результат работы данной программы. Его можно интерпретировать таким образом. Выполнено 2 теста (tests) и 9 сравнений (assertions). Обнаружено 0 сбоев (failures) и 0 ошибок (errors). Это означает, что программа теоретически работает и, наверное, все в порядке.

$ ruby test_number_decomposition.rb Loaded suite test_number_decomposition Started

Finished in 0.003189 seconds.

2 tests, 9 assertions, 0 failures, 0 errors $

Ради эксперимента искусственно внесем ошибку в метод prime?, сделав так, чтобы он считал единицу простым числом.

Запуск теста моментально обнаруживает ошибку, показав место, где произошел сбой – седьмую строку с выражением assert false == prime?(1) (отметим, что количество сбоев стало равным одному). Изучив тестовый случай, можно понять, где произошла ошибка и в чем причина. Теперь выполнять тесты стало очень легко.

$ ruby test_number_decomposition.rb Loaded suite test_number_decomposition Started.F

Finished in 0.061807 seconds.

1) Failure:

test_prime? (TestNumDecomposition) [test_number_decomposition.rb:7]: <false> is not true.

2 tests, 5 assertions, 1 failures, 0 errors $

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

3.2 Обработка исключительных ситуаций.

В большинстве современных языков программирования предусмотрена возможность работы с исключительными (особыми) ситуациями. В языке Ruby программисты могут работать как со встроенными ситуациями, так и с ситуациями, создаваемыми по указанию программиста при наличии того или иного события. Типичные встроенные ситуации – это «деление на ноль» (ZeroDivisionError) или «достижение конца файла» (EOFError).

Перейдем к простому примеру, в котором мы будем обрабатывать исключительную ситуацию «деление на ноль». При запуске данной программы возникает исключительная ситуация (ZeroDivisionError).

Для реализации механизма исключений в языке следующие ключевые слова: rescue, raise, ensure.

• rescue – поймать и обработать исключение, находящееся в блоке.

• raise – генерировать исключительную ситуацию.

• ensure – всегда выполнить код, заключенный в этот блок.

rescue ZeroDivisionError

puts "Произошло деление на ноль"

ensure

puts "Блок ensure всегда выполняется!"

end

Обработаем ситуацию, описанную выше, внеся опасный участок кода в блок обработки исключения rescue.

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

f = File.open("testfile") begin #.. выполнение rescue #.. обработка ошибки ensure # закрыть соединение с файлом f.close unless f.nil? end

    Будьте внимательны при использовании исключений. Использование конструкции rescue без указания конкретного исключения приводит к обработке всех ошибок.

Любая библиотека Ruby возбуждает исключения при возникновении любой ошибки, и вы также можете явно возбуждать ошибки в своем коде. Создать исключение программист может с помощью метода raise модуля Kernel (или его синонима fail). Рассмотрим его применение на следующих примерах:

raise

raise "Missing name" if name.nil?

if i >= names.size

     raise IndexError, "#{i} >= size(#{names.size})"

end

Первая форма просто перехватывает текущее исключение и позволяет тем или иным способом затем обработать его. Старайтесь не использовать форму возбуждения исключения без аргумента, так как она не информативна. Второй пример демонстрирует передачу сообщения при возбуждении исключения. Вид возбужденного исключения в этом случае можно узнать, определив тип переменной $!

sum=0

begin

     a.each{|x|sum+=1}

rescue

     puts sum

end

Обратите внимание на следующую программу. Она не должна работать, т.к. объект а не проинициализирован, но …

Бесконечный ввод данных, последовательности

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

sum=0

begin

     while true

           sum+=readline.to_f

     end

rescue EOFError

     puts "Сумма последовательности #{sum}"

end

Сумма элементов последовательности

Пусть в задаче требуется вычислить сумму всех элементов последовательности чисел, получаемых из входного потока. Мы вводим данные в бесконечном цикле и при получении нового числа прибавляем его значение к переменной sum. При завершении ввода печа тается значение переменной sum. (При вводе даннных с консоли нажатие Ctrl-D (CTRL-Z в Windows) завершает ввод.)

Обратим внимание на распространенную ошибку. При работе с последовательностью не следует использовать контейнеры (объекты классов Array или Hash) для ее хранения, так как мы считаем, что элементов может быть бесконечно много и памяти для хранения недостаточно. Следующая программа может аварийно завершиться, если входная последовательность содержит большое число членов.

arr=[] #Не последовательность

begin

     arr << readline.to_iwhiletrue

rescue EOFError

     sum=0

     arr.each{|v|sum+=v}

     puts sum

end

    Похожая на readline функция gets не возбуждает исключительной ситуации при прерывании ввода и, по этой причине, не может использоваться в задачах, требующих обработки исключений.

Среднее арифметическое элементов последовательности

Для решения этой задачи нам недостаточно одной переменной sum, так как функция, вычисляющая среднее арифметическое значение последовательности, не является индуктивной. Потребуется построить её индуктивное расширение. Для одной и той же функции f можно придумать разные расширения. Однако, наибольший практический интерес представляет такое индуктивное расширение, которое содержит минимум дополнительной информации.

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

sum, count = 0.0, 0

begin

while true

     sum += readline f

     o

     t

     count += 1

end

rescue EOFError

puts count == 0 ?

     0 :

     sum/count

end

В данной задаче минимальным индуктивным расширением является объединение двух индуктивных функций – количества элементов последовательности и их суммы.

Как правило, решение таких задач сводится к тому, что программист должен понять, какой информации (переменных) ему не хватает, и ввести ее в программу Более подробно об этом можно причитать в пособии Е.А. Роганова «Основы информатики и программирования» (стр. 132—138).

3.3 Тестирование последовательностей. Для тестирования программ, работающих с последовательностью, возможно применение следующего приёма. Продемонстрируем его на тестировании программы, находящей среднее арифметическое элементов последовательности.

require ’test/unit’

class TestAverage < Test::Unit::TestCase

     def setup # запуск программы с последовательностью @seq = IO.popen("ruby average.rb", "r+") end

def teardown # выключение программы (закрытие потока) @seq.close end

# метод направляющий поток данных в тестируемую

# программу и возвращающий результат в виде строки def sequence(*items)

items.each { |i| @seq.puts(i) }

     @seq.close_write @seq.read.chomp end

     def test_average

     assert_equal("2.5",sequence(1,2,3,4))

end

def test_empty

assert_equal("0", sequence()) end end

Отметим что функция sequence() возвращает результат выполнения программы average.rb в виде строки (выводимой коммандой puts). Таким образом, если бы оператор печати выглядет так:

puts "average = #{count == 0 ? 0 : sum/count}"

то тестовый случай должен принять вид

assert_equal(sequence(1,2,3,4),"average = 2.5")

Задача 1. Найдите максимальный элемент последовательности чисел.

Задача 2. Найдите второй по величине элемент последовательности.

Тестирование последовательностей

Задача 3. Напишите программу, определяющую, является ли последовательность чисел возрастающей?

Задача 4. Найдите наибольший общий делитель последовательности целых чисел.

Задача 5. Найдите сумму всех попарных произведений элементов последовательности чисел.