Хэл Фултон - Программирование на языке Ruby
Последний квадрант (LR) заполняется сравнительно легко. Мы форматируем и заключаем в прямоугольник со скругленными углами текст речи, которую президент Линкольн произнес в Геттисберге.
Сохранение PDF-документа — воплощенная простота. Если нужно записать его на диск, мы вызываем метод save_as объекта PDF:
pdf.save_as("4page.pdf")
Нетрудно также отправить PDF-документ браузеру из CGI-программы:
require 'cgi'
cgi = CGI.new
out = pdf.render
puts <<-EOS
Content-Type: application/pdf
Content-Disposition: inline; filename="4page.pdf"
Size: #{out.size}
EOS
Конечно, в этом разделе мы сумели затронуть лишь малую толику библиотеки PDF::Writer. Дополнительную информацию ищите в онлайновой документации. Если вы знакомы с форматом PDF, имейте в виду, что библиотека еще развивается и пока не поддерживает спецификацию в полном объеме.
15.5. Заключение
В этой главе мы показали, как с помощью библиотеки REXML можно разбирать XML-документы, представленные в виде дерева DOM или потока. Познакомились мы и с интерфейсом REXML к языку XPath.
Был продемонстрирован разбор информации из новостных каналов, представленных в формате на базе XML. Библиотека rss умеет работать только с форматом RSS, а библиотека feedtools понимает форматы RSS и Atom (и умеет преобразовывать из одного в другой).
Мы также видели, как можно читать и манипулировать графическими изображениями разного формата с помощью библиотеки RMagick. Рассмотрели мы и API рисования, позволяющий включать в изображение произвольный текст и геометрические фигуры. Наконец, мы показали, как с помощью библиотеки PDF::Writer можно создавать из программы сложные PDF-документы высокого качества.
Следующая глава посвящена совсем другой теме. Речь пойдет об эффективном тестировании и отладке написанных на Ruby программ.
Глава 16. Тестирование и отладка
Неполадки в блоке АЕ-35. В ближайшие семьдесят два часа блок может отказать.
Артур Кларк, «Космическая Одиссея 2001 года»Тестирование — вещь важная. Все компетентные программисты об этом знают, хотя не всегда этот вопрос стоит для них на первом месте.
Конечно, исчерпывающее тестирование, как правило, невозможно. Программа сколько-нибудь заметного размера на протяжении своего жизненного цикла обязательно преподнесет сюрпризы. Максимум, что мы можем сделать, — тестировать тщательно и избирательно, стараясь проверить как можно больше.
Исторически сложилось так, что программисты не всегда тестируют как положено. Объясняют это обычно тем, что тесты трудно готовить и прогонять, что вся процедура требует ручного вмешательства или отнимает слишком много времени.
В 1990 году в сообществе программистов стала распространяться «культура тестирования». Идеи экстремального программирования и управляемой тестами разработки начали овладевать умами разработчиков по всему миру.
Являетесь ли вы твердокаменным приверженцем идеологии «тестируй с самого начала», не так существенно. Важно, что любой человек может воспользоваться инструментами, которые позволяют автоматизировать тестирование, упростив написание и прогон тестов.
Такие инструменты, как Test::Unit и ZenTest, написать на Ruby было проще в силу динамичности и гибкости языка. Не менее легко и (посмею ли сказать?) приятно ими пользоваться. Внес изменение в программу, а потом смотришь, как все тесты успешно доходят до конца, — положительно в этом что-то есть!
Помимо этих инструментов в Ruby есть еще немало программ и библиотек для отладки, профилирования и испытания различных путей исполнения. Эта глава посвящена обзору имеющихся средств.
16.1. Библиотека Test::Unit
«Стандартный» способ автономного тестирования компонентов в Ruby — библиотека Test::Unit Натаниэля Тэлбота (Nathaniel Talbott). Она была включена в дистрибутив Ruby еще в 2001 году.
В этой библиотеке для анализа тестового кода применяется отражение. Когда вы создаете подкласс класса Test::Unit::TestCase, все методы, имена которых начинаются с test, считаются тестовыми.
require 'test/unit'
class TC_MyTest < Test::Unit::TestCase
def test_001
# ...
end
def test_002
# ...
end
# ...
end
Методы необязательно нумеровать, как показано в этом примере. Это мое личное соглашение, но, конечно, есть и другие.
Нежелательно и, пожалуй, даже неправильно составлять тесты так, чтобы их поведение зависело от порядка запуска. Однако Test::Unit прогоняет их в алфавитном (лексикографическом) порядке, поэтому, нумеруя свои методы, я вижу, как они выполняются в определенной последовательности.
Я также предпочитаю включать некий «заголовок» в имя метода (описывающий его область действия или назначение):
def test_053_default_to_current_directory
# ...
end
def test_054_use_specified_directory
# ...
end
Кроме прочего, неплохо оставлять хотя бы однострочный комментарий, касающийся цели и смысла теста. Вообще говоря, у каждого теста должна быть только одна цель.
А если нужно организовать некую среду выполнения, для чего требуется время? Неразумно делать это для каждого теста, и мы не вправе завести для данной цели отдельный метод (поскольку поведение не должно зависеть от порядка прогона).
Если всем тестам нужна особая среда, можно воспользоваться методами класса setup и teardown. Возможно, вам это покажется странным, но вызываются они для каждого теста. Если вы хотите выполнить настройку один раз, перед прогоном одного конкретного или всех тестов, то можете поместить соответствующий код в тело класса раньше всех тестовых методов (или даже до самого класса).
А если после выполнения всех тестов нужно разрушить созданную среду? По техническим причинам (так уж работает библиотека Test::Unit) сделать это трудно. «Самый лучший» способ — переопределить метод run всего комплекта тестов (но не метод класса run), обернув его функциональность. Рассмотрим пример в листинге 16.1.
Листинг 16.1. Подготовка и разрушение среды исполненияrequire 'test/unit'
class MyTest < Test::Unit::TestCase
def self.major_setup
# ...
end
def self.major_teardown
# ...
end
def self.suite
mysuite = super # Вызвать метод suite родителя.
def mysuite.run(*args) # Добавить синглетный метод
MyTest.major_setup
super
MyTest.major_teardown
end
mysuite # и вернуть новое значение.
end
def setup
# ...
end
def teardown
# ...
end
def test_001
# ...
end
def test_002
# ...
end
# ...
end
Вряд ли вы будете поступать так часто. О методе suite мы поговорим чуть позже, а пока продолжим рассмотрение отдельных тестов.
Что должно входить в тест? Нужно как-то решить, прошел он или нет. Для этой цели применяются утверждения.
Простейшее утверждение — это метод assert. Он принимает проверяемый параметр и еще один необязательный параметр (сообщение). Если значение параметра истинно (то есть все, кроме false и nil), тест прошел. В противном случае тест не прошел — тогда печатается сообщение, если оно было задано.
Есть и другие методы для формулирования утверждений. Обратите внимание, что «ожидаемое» значение всегда предшествует «фактическому».
assert_equal(expected, actual) # assert(expected==actual)
assert_not_equal(expected, actual) # assert(expected!=actual)
assert_match(regex, string) # assert(regex =~ string)
assert_no_match(regex, string) # assert(regex string)
assert_nil(object) # assert(object.nil?)
assert_not_nil(object) # assert(!object.nil?)
Некоторые утверждения носят более объектно-ориентированный характер:
assert_instance_of(klass, obj) # assert(obj.instance_of? klass)
assert_kind_of(klass, obj) # assert(obj.kind_of? klass)
assert_respond_to(obj, meth) # assert(obj.respond_to? meth)
Другие относятся к исключениям и символам, которые генерируются методом throw. Понятно, что такие методы принимают блок.
assert_nothing_thrown { ... } # Не было throw.
assert_nothing_raised { ... } # Не было raise.
assert_throws(symbol) { ... } # Символ в результате throw.
assert_raises(exception) { ... } # Исключение в результате raise.
Есть еще несколько утверждений, но эти применяются чаще всего и отвечают почти всем потребностям. Дополнительную информацию можно найти в онлайновой документации на сайте http://ruby-doc.org.