|
||||||||||||||||||
|
Глава 16. Тестирование и отладка
Тестирование — вещь важная. Все компетентные программисты об этом знают, хотя не всегда этот вопрос стоит для них на первом месте. Конечно, исчерпывающее тестирование, как правило, невозможно. Программа сколько-нибудь заметного размера на протяжении своего жизненного цикла обязательно преподнесет сюрпризы. Максимум, что мы можем сделать, — тестировать тщательно и избирательно, стараясь проверить как можно больше. Исторически сложилось так, что программисты не всегда тестируют как положено. Объясняют это обычно тем, что тесты трудно готовить и прогонять, что вся процедура требует ручного вмешательства или отнимает слишком много времени. В 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. Имеется еще метод flunk, который всегда завершается неудачно. Можно считать, что это некий вид заглушки. Если при запуске тестового файла вы ничего специально не указываете, то по умолчанию вызывается консольный исполнитель тестов. Это возвращает нас к старой доброй технологии 1970-х годов. Имеются и другие исполнители, например графический Test::Unit::UI::GTK::TestRunner. Любой исполнитель тестов можно вызвать, обратившись к его методу run, которому передается специальный параметр, описывающий набор тестов: class MyTests < Test::Unit::TestCase # ... end # Явное указание исполнителя тестов... runner = Test::Unit::UI::Console::TestRunner runner.run(MyTests) Параметром может быть любой объект, обладающий методом suite, который возвращает объект, представляющий комплект тестов. Что все это означает? Познакомимся к понятием комплекта тестов ближе. Оказывается, комплект тестов может состоять из набора тестов или набора подкомплектов. Следовательно, можно сгруппировать тесты так, что будет прогоняться либо только один набор, либо сразу все. Пусть, например, есть три набора тестов, и вы хотите прогнать их как единый комплект. Можно было бы поступить так: require 'test/unit/testsuite' require 'tc_set1' require 'tc_set2' require 'ts_set3' class TS_MyTests def self.suite mysuite = Test::Unit::TestSuite.new mysuite << TC_Set1.suite mysuite << TC_Set2.suite mysuite << TS_Set3.suite return mysuite end end Test::Unit::UI::Console::TestRunner.run(TS_MyTests) Но такая сложность ни к чему. Имея отдельные наборы тестов, библиотека Test::Unitв состоянии просмотреть пространство объектов и объединить их все в один комплект. Поэтому следующий код тоже будет работать (и даже вызывать подразумеваемый по умолчанию исполнитель тестов): require 'test/unit' require 'tc_set1' require 'tc_set2' require 'ts_set3' Библиотека Test::Unitрасполагает и другими возможностями, а в дальнейшем, вероятно, будет усовершенствована. Самую свежую информацию ищите в сети. 16.2. Комплект инструментов ZenTestЭтот великолепный инструментарий написал Райан Дэвис (Ryan Davis). Основной инструмент ( zentest) — это исполняемая программа, которая генерирует файл с тестами на основе анализа вашего кода. Тестируемый класс (class under test — CUT) служит основой тестового класса (test class — ТС). На каждом уровне области видимости в начало имени класса добавляется строка Test, а в начало имени метода — строка test_. Иногда имена методов приходится «подправлять», например в случае с методом ==(к имени которого нельзя добавлять никакой префикс) или если имя метода оканчивается на ?, ! или =. В листинге 16.2 приведен пример подлежащего тестированию кода: Листинг 16.2. Класс, подлежащий тестированию class Alpha class Beta attr_accessor :foo, :bar def initialize end def foo? @foo end end def initialize end def process end def process! end def ==(other) end def ===(other) end end После запуска команды zentest file.rb >tfile.rbполучится файл, показанный в листинге 16.3. Листинг 16.3. Результат работы ZenTest # Code Generated by ZenTest v. 3.2.0 # classname: asrt / meth = ratio% # Alpha::Beta: 0 / 7 = 0.00% require 'test/unit' unless defined? $ZENTEST and $ZENTEST class TestAlpha < Test::Unit::TestCase def test_process raise NotImplementedError, 'Need to write test_process' end def test_process_bang raise NotImplementedError, 'Need to write test_process_bang' end end module TestAlpha class TestBeta < Test::Unit::TestCase def test_bar raise NotImplementedError, 'Need to write test_bar' end def test_bar_equals raise NotImplementedError, 'Need to write test_bar_equals' end def test_foo raise NotImplementedError, 'Need to write test_foo' end def test_foo_eh raise NotImplementedError, 'Need to write test_foo_eh' end def test_foo_equals raise NotImplementedError, 'Need to write test_foo_equals' end end end # Number of errors detected: 9 Обратите внимание, что каждый тестовый метод возбуждает исключение (предложение raise). Идея в том, что все тесты завершаются неудачно, пока вы явно не напишете код. Исходный файл почему-то не включается в тестовый. Можно поместить в начало тестового файла директиву require 'file'или эквивалентную ей (предварительно затребовав test/unit). Тогда тестовый код увидит определения ваших классов. В командной строке можно указать и второй параметр. Если добавить код в тестируемый класс, то тестовые классы окажутся устаревшими. Чем обновлять их вручную, можно сгенерировать только «обновления»: zentest file.rb tfile.rb >tfile2.rb В комплект входит еще одна полезная программа: unit_diff. Рассмотрим простое утверждение assert_equal ("foo", "bar"). Оно приводит к печати следующего сообщения: 1) Failure: testme(Foo) [(irb):7]: <"foo"> expected but was <"bar">. Тут все просто и понятно. Но предположим, что каждая из переданных строк (string) состояла из нескольких строчек (line), а различие начиналось только в седьмой строчке. Программа unit_diffкак раз призвана навести порядок в таком плохо читаемом тексте. Она работает аналогично утилите diff, имеющейся в UNIX; вызывать ее следует как фильтр после обычной тестовой программы. ruby testfile.rb | unit_diff Программа понимает следующие флаги: -h Справка о порядке запуска -v Номер версии -b Не считать различными фрагменты, различающиеся только по количеству пробелов -c Выполнять контекстное сравнение -k Не удалять временные файлы -l Включать в дельту номера строк -u Выполнять унифицированное сравнение Программа autotestнаблюдает за всеми вашими комплектами тестов и запускает те, которые были недавно изменены. Она предназначена для тех лентяев, которым трудно даже ввести имя файла для прогона тестов. Для пользования этим инструментом нужно соблюдать некоторые соглашения об именах. Правила простые: • все тесты должны находиться в каталоге test; • имена всех файлов должны начинаться с Test_; • имена классов должны начинаться с Test; • подлежащий тестированию код должен находиться в каталоге lib; • файлам в каталоге libдолжны соответствовать файлы в каталоге test(их имена, конечно, должны начинаться с test_). Будучи запущена, программа autotestпрогоняет тесты по мере их обновления. Если какой-то тест завершается неудачно, она будет прогонять его снова и снова, пока вы не исправите ошибку. Она начнет «сверху», если нажать комбинацию клавиш Ctrl+C, и завершится, если нажать Ctrl+C во второй раз. Программа multirubyпозволяет тестировать код относительно разных версий Ruby. Она входит в комплект ZenTest, но пока еще работает не очень стабильно и плохо документирована. 16.3. Работа с отладчиком RubyЧестно говоря, отладчик Ruby не особенно популярен. Лично я им не пользуюсь и встречал не так уж много людей, которые с ним работали. Но сознавать, что он есть, приятно. Ниже приводится краткое описание работы с ним. Для вызова отладчика нужно просто затребовать библиотеку debug— например, в командной строке: ruby -rdebug rayfile.rb В ответ на приглашение вида ( rdb:1) вы можете вводить различные команды, например listдля получения текста всей программы или ее части, step для пошагового исполнения метода и т.д. Некоторые команды перечислены в таблице 16.1 (полужирным шрифтом набраны сокращения). Таблица 16.1. Наиболее употребительные команды отладчика
В листинге 16.4 приведен код простой программы (даже слишком простой, чтобы ее отлаживать). Листинг 16.4. Простая программа для демонстрации работы отладчикаSTDOUT.sync = true def palindrome?(word) word == word.reverse end def signature(w) w.split("").sort.join end def anagrams?(w1,w2) signature(w1) == signature(w2) end print "Give me a word: " w1 = gets.chomp print "Give me another word: " w2 = gets.chomp verb = palindrome?(w1) ? "is" : "is not" puts "'#{w1}' #{verb} a palindrome." verb = palindrome?(w2) ? "is" : "is not" puts "'#{w2}' #{verb} a palindrome." verb = anagrams?(w1,w2) ? "are" : "are not" puts "'{w1}' and '#{w2}' #{verb} anagrams." В листинге 16.5 показан полный сеанс отладки. Некоторые места не слишком понятны, потому что на консоль выводят данные и сама программа, и отладчик. Листинг 16.5. Сеанс отладки простой программы$ ruby -rdebug db.rb Debug.rb Emacs support available. db.rb:1:STDOUT.sync = true (rdb:1) b palindrome? Set breakpoint 1 at db.rb:palindrome? (rdb:1) b anagrams? Set breakpoint 2 at db.rb:anagrams? (rdb:1) b Breakpoints: 1 db.rb:palindrome? 2 db.rb:anagrams? (rdb:1) n db.rb:3:def palindrome?(word) (rdb:1) n db.rb:7:def signature(w) (rdb:1) n db.rb:11:def anagrams?(w1,w2) (rdb:1) n db.rb:15:print "Give me a word: " (rdb:1) n Give me a word: db.rb:16:w1 = gets.chomp (rdb:1) live db.rb:16:undefined local variable or method 'live' for main:Object (rdb:1) n live db.rb:18:print "Give me another word: " (rdb:1) n Give me another word: db.rb:19:w2 = gets.chomp (rdb:1) n evil db.rb:21:verb = palindrome?(w1) ? "is" : "is not" (rdb:1) с Breakpoint 1, palindrome? at db.rb:palindrome? db.rb:3:def palindrome?(word) (rdb:1) n db.rb: 4: word == word.reverse (rdb:1) word "live" (rdb:1) n db.rb: 22: puts "'#{w1}' #{verb} a palindrome." (rdb:1) verb "is not" (rdb:1) n 'live' is not a palindrome. db.rb:24:verb = palindrome?(w2) ? "is" : "is not" (rdb:1) n db.rb:24:verb = palindrome?(w2) ? "is" : "is not" (rdb:1) n Breakpoint 1, palindrome? at db.rb:palindrome? db.rb:3:def palindrome?(word) (rdb:1) n db.rb:4: word == word.reverse (rdb:1) c 'evil' is not a palindrome. Breakpoint 2, anagrams? at db.rb:anagrams? db.rb:11:def anagrams?(w1,w2) (rdb:1) n db.rb:12: signature(w1) == signature(w2) (rdb:1) n db.rb:28:puts "'#{w1}' and '#{w2}' #{verb} anagrams." (rdb:1) verb "are" (rdb:1) c 'live' and 'evil' are anagrams. Если вы затребуете другие библиотеки, то, возможно, придется в самом начале «перешагнуть» через несколько методов. Я рекомендую перед началом отладки установить где-нибудь в собственном коде точку прерывания, а потом выполнить команду continueдля останова в этой точке. Отладчик понимает и много других команд. Можно просматривать стек вызовов и перемещаться по нему. Можно «наблюдать» за выражениями и автоматически останавливать выполнение, как только выражение изменится. Можно добавлять выражения в «отображаемый список». Поддерживается работа с несколькими потоками и переключение между ними. Вероятно, полной и качественной документации вы нигде не найдете. Если возникнет такая необходимость, рекомендуется обратиться к оперативной справке и действовать методом проб и ошибок. Современные отладчики имеют графический интерфейс. Если вам необходим такой инструмент, загляните в главу 21, где обсуждаются интегрированные среды разработки для Ruby. 16.4. Использование irb в качестве отладчикаБиблиотеку ruby-breakpointнаписал Флориан Гросс (Florian Gross). Этот великолепный, несмотря на малый объем, инструмент позволяет расставлять в программе точки прерывания методом breakpoint. Когда в процессе исполнения встречается точка прерывания, запускается сеанс irb(программа интерактивной работы с Ruby irbподробно рассматривается в главе 21). Эта библиотека не входит в стандартный дистрибутив. Установить ее можно, например, выполнив команду gem install ruby-breakpoint. Внесем несколько изменений в программу из листинга 16.4. Поместим в начало директиву require 'breakpoint'и добавим вызов метода breakpointпосле обоих обращений к gets: require 'breakpoint' # ... w2 = gets.chomp breakpoint # ... Теперь запустим ее. В следующем протоколе сеанса показано, как мы входим в irb, после чего можем делать все что угодно — в частности, вызывать ранее определенные методы и изменять значения переменных. $ ruby myprog.rb Give me a word: parental Give me another word: prenatal Executing break point at myprog.rb:23 irb(main):001:0> w1 => "parental" irb(main):002:0> w2 => "prenatal" irb(main):003:0> palindrome?(w1) => false irb(main):004:0> palindrome?("detartrated") => true irb(main):005:0> signature(w1) => "aaelnprt" irb(main):006:0> quit 'parental' is not a palindrome. 'prenatal' is not a palindrome. 'parental' and 'prenatal' are anagrams. Особенно подкупает, что отлаживаемая программа может быть не только командной или текстовой. Существует клиент drb (распределенный Ruby), который позволяет удаленно отлаживать программу Ruby, работающую в другом процессе. Чтобы воспользоваться этой возможностью, нужно включить вызов следующего метода в отлаживаемую программу (естественно, до первого обращения к методу breakpoint): Breakpoint.activate_drb("druby://127.0.0.1:2001", "localhost") # Запустить сервер на порту 2001 машины localhost. Запустите клиент командой breakpoint_client. Каждые три секунды он будет пытаться установить соединение с сервером, пока это не получится или вы не завершите его принудительно. $ breakpoint_client druby://localhost:2001 No connection to breakpoint service at druby://localhost:2001 (DRb::DRbConnError) Tries to connect will be made every 3 seconds... После установления соединения вы можете и не получить приглашение irb. Программа будет выполняться до точки прерывания — вот тогда-то вы и увидите приглашение. Дополнительную информацию об этой библиотеке поищите в документации, которая входит в комплект поставки. 16.5. Измерение покрытия кодаОчень полезно знать, какие части программы не были протестированы, а следовательно, нуждаются в автономных тестах. Иногда и сам инструмент для замера покрытия может обнаружить ошибки. Допустим, в программе есть предложение if, которое «должно» исполняться примерно в половине всех случаев. Если выясняется, что оно не исполняется никогда, значит, имеет место ошибка. Командную утилиту rcov(и соответствующую библиотеку) написал Маурисио Фернандес (Mauricio Fernandez). Устанавливается она в виде gem-пакета. В простейшем случае для ее запуска достаточно указать имя вашей программы в качестве параметра: rcov myfile.rb Одновременно с исполнением вашей программы rcovбудет собирать статистику. По умолчанию она создает каталог coverage, в котором вы найдете HTML-файлы. В файле index.htmlпредставлены сводные результаты и ссылки на исходные тексты, где строки, которые хотя бы раз исполнялись, подсвечены. Из-за цветового кодирования трудно привести черно-белый снимок с экрана. Но сам инструмент настолько прост, что, потратив пару минут, вы сможете увидеть все сами. Хотя программа rcovполезна даже в стандартном режиме, она понимает порядка 30 различных параметров. Можно указать каталог для выходных файлов, образцы имен файлов, для которых собирать и не собирать статистику, задать режим сортировки по именам файлов и многое другое. Можно выводить результаты в текстовом виде и даже запросить цветную диаграмму покрытия. Рекомендую прочитать поставляемую документацию, запросить справку командой rcov -hи… получать удовольствие. Можно использовать rcovи в качестве библиотеки для написания аналогичных инструментов анализа. Ее API состоит из трех основных классов: • Rcov::FileStatisticsпозволяет отличить исполняемые предложения от комментариев (и тем самым уточнить статистику покрытия); • Rcov::CodeCoverageAnalyzerприменяется для трассировки выполнения, возвращает информацию о покрытии и счетчики выполненных предложений; • Rcov::CallSiteAnalyzerнужен для того, чтобы понять, где определены методы и откуда они вызываются. Обсуждение API далеко выходит за рамки этого раздела. Почитайте документацию и начинайте экспериментировать. 16.6. Измерение производительностиЯ не люблю уделять слишком много внимания оптимизации скорости. В общем случае нужно правильно выбрать алгоритм и придерживаться здравого смысла. Конечно, быстродействие имеет значение. Иногда даже очень большое. Однако начинать думать об этом на раннем этапе цикла разработки — ошибка. Как говорится, «преждевременная оптимизация — источник всех зол»; эту мысль впервые высказал Хоар (Hoare), а потом подтвердил Кнут (Knuth). Или, перефразируя, сначала пусть работает правильно, а уж потом быстро». На уровне отдельного приложения эта рекомендация обычно оказывается хорошим эвристическим правилом, хотя для больших систем она, быть может, и не так актуальна. Я бы еще добавил: «Не оптимизируйте, пока не измерите». Это не такое уж серьезное ограничение. Просто не приступайте к переработке ради скорости, пока не ответите на два вопроса: «Действительно ли программа работает медленно? Какие именно ее части снижают производительность?» Второй вопрос важнее, чем кажется на первый взгляд. Программисты часто уверены, что и так знают, на что программа тратит большую часть времени, но специальные исследования убедительно свидетельствуют о том, что в среднем эти догадки имеют очень мало общего с действительностью. «Теоретическая» оптимизация для большинства из нас — плохая идея. Нам нужны объективные измерения. Профилировщик нужен. В комплект поставки Ruby входит профилировщик profile. Для его вызова достаточно включить библиотеку: ruby -rprofile myprog.rb Рассмотрим листинг 16.6. Эта программа открывает файл /usr/share/dict/wordsи ищет в нем анаграммы. Затем смотрит, у каких слов оказалось больше всего анаграмм, и распечатывает их. Листинг 16.6. Поиск анаграмм в словаре words = File.readlines("/usr/share/dict/words") words.map! {|x| x.chomp } hash = {} words.each do |word| key = word.split("").sort.join hash[key] ||= [] hash [key] << word end sizes = hash.values.map {|v| v.size } most = sizes.max list = hash.find_all {|k,v| v.size == most } puts "Ни у одного слова нет более #{most-1} анаграмм." list.each do |key,val| anagrams = val.sort first = anagrams.shift puts "Слово #{first} имеет #{most-1) анаграмм:" anagrams.each {|a| puts " #{a}" } end num = 0 hash.keys.each do |key| n = hash[key].size num += n if n > 1 end puts puts "Всего слов в словаре: #{words.size}," puts "из них имеют анаграммы: #{num}." Наверняка вам интересно, какие получились результаты. Вот какие: Ни у одного слова нет более 14 анаграмм. Слово alerts имеет 14 анаграмм: alters artels estral laster lastre rastle ratels relast resalt salter slater staler stelar talers Всего слов в словаре: 483523, из них имеют анаграммы: 79537. На моем компьютере этот файл содержит более 483000 слов, и программа работала чуть меньше 18 секунд. Как вы думаете, на что ушло это время? Попробуем выяснить. Профилировщик выдал более 100 строк, отсортированных в порядке убывания времени. Мы покажем только первые 20: % cumulative self self total time seconds seconds calls ms/call ms/call name 42.78 190.93 190.93 15 12728.67 23647.33 Array#each 10.78 239.04 48.11 1404333 0.03 0.04 Hash#[] 7.04 270.48 31.44 2 15720.00 25575.00 Hash#each 5.66 295.73 25.25 483523 0.05 0.05 String#split 5.55 320.51 24.78 1311730 0.02 0.02 Array#size 3.64 336.76 16.25 1 16250.00 25710.00 Array#map 3.24 351.23 14.47 483524 0.03 0.03 Array#sort 3.12 365.14 13.91 437243 0.03 0.03 Fixnum#== 3.04 378.72 13.58 483526 0.03 0.03 Array#join 2.97 391.98 13.26 437244 0.03 0.03 Hash#default 2.59 403.53 11.55 437626 0.03 0.03 Hash#[]= 2.43 414.38 10.85 483568 0.02 0.02 Array#<< 2.29 424.59 10.21 1 10210.00 13430.00 Array#map! 1.94 433.23 8.64 437242 0.02 0.02 Fixnum#<=> 1.86 441.54 8.31 437244 0.02 0.02 Fixnum#> 0.72 444.76 3.22 483524 0.01 0.01 String#chomp 0.11 445.26 0.50 4 125.00 125.00 Hash#keys 0.11 445.73 0.47 1 470.00 470.00 Hash#values 0.06 446.00 0.27 1 270.00 270.00 IO#readlines 0.05 446.22 0.22 33257 0.01 0.01 Fixnum#+ Видно, что больше всего времени программа тратит в методе Array#each. Это понятно: ведь цикл выполняется для каждого слова и на каждой итерации делает довольно много. Среднее значение в данном случае сбивает с толку, поскольку почти все время уходит на первый вызов each, а остальные 14 (см. anagrams.each) выполняются очень быстро. Мы также видим, что Hash#[]— дорогая операция (главным образом потому что часто выполняется); на 1.4 миллиона вызовов было потрачено почти 11 секунд. Обратите внимание, что метод readlinesоказался чуть ли не в самом конце списка. Эта программа тратит время не на ввод/вывод, а на вычисления. На чтение всего файла ушло всего-то четверть секунды. Но этот пример не показывает, в чем истинная ценность профилирования. В программе нет ни методов, ни классов. На практике вы, скорее всего, увидите свои методы среди системных. И тогда будете точно знать, какие из ваших методов находятся в числе первых 20 «пожирателей времени». Надо ясно понимать, что профилировщик Ruby (видно, по иронии судьбы) работает медленно. Он подключается к программе во многих местах и следит за ее выполнением на низком уровне (причем сам написан на чистом Ruby). Так что не удивляйтесь, если ваша программа в ходе сеанса профилирования будет работать на несколько порядков медленнее. В нашем примере она работала 7 минут 40 секунд (460 секунд), то есть в 25 раз медленнее обычного. Помимо профилировщика, есть еще один низкоуровневый инструмент — стандартная библиотека benchmark, которая тоже полезна для измерения производительности. Один из способов ее применения — вызвать метод Benchmark.measureи передать ему блок. require 'benchmark' file = "/usr/share/dict/words" result = Benchmark.measure { File.readlines(file) } puts result # Выводится: 0.350000 0.070000 0.420000 ( 0.418825) Этот метод выводит следующую информацию: • время, затраченное процессором в режиме пользователя (в секундах); • время, затраченное процессором в режиме ядра (в секундах); • полное затраченное время — сумму вышеупомянутых величин; • время работы программы (по часам). Для сравнения производительности отдельных участков удобен метод Benchmark.bm. Передайте ему блок, а он сам передаст блоку объект формирования отчета. Если вызвать этот объект, передав ему метку и блок, то он выведет метку, а за ней временные характеристики блока. Пример: require 'benchmark' n = 200_000 s1 = "" s2 = "" s3 = "" Benchmark.bm do |rep| rep.report("str << ") { n.times { s1 << "x" } } rep.report("str.insert ") { n.times { s3.insert(-1,"x") } } rep.report("str += ") { n.times { s2 += "x" } } end Здесь мы сравниваем три способа добавить символ в конец строки, дающие один и тот же результат. Чтобы можно было получить более точные цифры, каждая операция выполняется 200000 раз. Вот что вышло: user system total real str << 0.180000 0.000000 0.180000 ( 0.174697) str.insert 0.200000 0.000000 0.200000 ( 0.200479) str += 15.250000 13.120000 28.370000 (28.375998) Обратите внимание, что последний вариант на два порядка медленнее остальных. Почему? Какой урок можно извлечь отсюда? Вы можете предположить, что оператор +почему-то работает медленно, но дело в другом. Это единственный из трех способов, который не работает с одним и тем же объектом, а каждый раз создает новый. Стало быть, вывод такой: создание объекта — дорогая операция. Библиотека Benchmark может преподать много подобных уроков, но я все же рекомендую сначала заняться высокоуровневым профилированием. 16.7. Объекты печатиМетод inspect(и вызывающий его метод p) предназначен для вывода объектов в виде, понятном человеку. В этом смысле он является связующим звеном между тестированием и отладкой, поэтому рассмотрение его в этой главе оправданно. Проблема в том, что результат, формируемый методом p, бывает трудно читать. Из-за этого и появилась библиотека pp, добавляющая одноименный метод. Рассмотрим следующий искусственный пример объекта my_obj: class MyClass attr_accessor :alpha, :beta, :gamma def initialize(a,b,c) @alpha, @beta, @gamma = a, b, с end end x = MyClass.new(2, 3, 4) y = MyClass.new(5, 6, 7) z = MyClass.new(7, 8, 9) my_obj = { x => y, z => [:p, :q] } p my_obj Вызов метода pпечатает следующее: {#<MyClass:0xb7eed86c @beta=3, @alpha=2, @gamma=4>=>#<MyClass:0xb7eed72c @beta=6, @alpha=5, @gamma=7>, #<MyClass:0xb7eed704 @beta=8, @alpha=7 , @gamma=9>=>[:p, :q]} Все правильно и в общем-то даже читаемо. Но… некрасиво. А давайте затребуем библиотеку ppи воспользуемся предоставляемым ей методом pp: require 'pp' # ... pp my_obj Теперь вывод приобретает такой вид: {#<MyClass:0xb7f7a050 @alpha=7, @beta=8, @gamma=9>=>[:p, :q], #<MyClass:0xb7f7a1b8 @alpha=2, @beta=3, @gamma=4>=> #<MyClass:0xb7f7a078 @alpha=5, @beta=6, @gamma=7>} Мы получили хотя бы пробелы и разбиение на строки. Уже лучше. Но можно пойти еще дальше. Предположим, что в классе MyClassопределен специальный метод pretty_print: class MyClass def pretty_print(printer) printer.text "MyClass(#@alpha, #@beta, #@gamma)" end end Аргумент printer передается вызывающей программой (или методом pp). Это аккумулятор текста, являющийся экземпляром класса PP; мы вызываем его метод textи передаем ему текстовое представление self. Вот что получается в результате: {MyClass(7, 8, 9)=>[:p, :q] , MyClass(2, 3, 4)=>MyClass(5, 6, 7)} Разумеется, можно настроить поведение по своему вкусу. Можно, например, печатать переменные экземпляра на разных строчках с отступами. На самом деле в библиотеке ppесть много средств для подготовки ваших классов к совместной работе с методом pp. Методы object_group, seplist, breakableи прочие позволяют управлять расстановкой запятых, разбиением на строки и другими способами форматирования. Дополнительную информацию можно найти в документации на сайте http://ruby-doc.org. 16.8. ЗаключениеВ этой главе мы рассмотрели некоторые подходы к тестированию (преимущественно, к автономному тестированию компонентов). Мы познакомились с библиотекой Test::Unitи комплектом инструментов ZenTest. Мы бросили беглый взгляд на отладчик Ruby, а также показали, как с помощью библиотеки ruby-breakpointможно переходить в интерактивную оболочку irbдля проведения сеанса отладки. Мы рассмотрели инструмент для анализа покрытия кода rcovи обсудили, для чего могут понадобиться такие измерения. Наконец, остановились на профилировании и замере временных характеристик программы на Ruby. Предположим, что вы работаете над программой, которую собираетесь предложить обществу. Что вы делаете по завершении тестирования? Пришло время задуматься над оформлением дистрибутивного пакета и способами его распространения. Этим мы и займемся в следующей главе. |
|
||||||||||||||||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх |
||||||||||||||||||
|