Статьи

Испытание на отвлечение

Рубин и TDD неразделимы. Язык пригоден для тестирования управляемой разработки до такой степени, что он поставляется с библиотекой Test::Unit качестве стандарта.

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

На протяжении всей статьи я буду использовать MiniTest :: Spec . Это очень крутой «фреймворк» в виде спецификации, который также входит в стандартную комплектацию Ruby 1.9. Я люблю RSpec, и наличие такого же синтаксиса у меня под рукой — большой плюс. Но выбранный вами тестовый фреймворк на самом деле не имеет значения, все обсуждаемое может быть применено Тем не менее, я настоятельно рекомендую попробовать MiniSpec.

Почему все эти тесты?

Мы уже говорили, что тестирование вашего кода делает его более надежным и улучшает качество. Ну, это не совсем верно. Вы не получаете надежность или качество бесплатно, так как ваш код будет таким же хорошим или надежным, как и ваши тесты.

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

Как я уверен, вы уже знаете: «Мы получаем намного больше».

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

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

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

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

Тестирование тоже весело. Но давайте сосредоточимся на том, что мы только что говорили о TDD.

Красный, зеленый, начать работать

Цитата «Red, Green, Refactor» делает весь процесс TDD почти тривиальным. Вы пишете тест, смотрите, как он проваливается, и делаете как можно меньше, чтобы он прошел. Теперь у нас есть пара вариантов.

  1. Переработать тест?
  2. Добавить больше тестов?
  3. Рефакторинг кода под зеленым светом?

Я уверен, что мы все читали это раньше и думали: «Да, без проблем». Но так ли просто, когда мы подходим к механике написания этих маленьких кусочков магии? Ну, тесты могут быстро стать совершенно расстраивающими и непроизводительными делами. Они становятся поспешными, монолитами мусора, которые просто ставят галочку в «Контрольном списке хорошего разработчика», который существует только в наших собственных головах, и мы переходим к нежеланию снова играть в этот кусок кода.

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

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

 Test Unit 101 WIGGUM RALF 13MALE COYOTE WYLIE 72MALE RABBIT JESSICA 25FEMALE 003 

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

Итак, давайте посмотрим на первый проход тестового кода для достижения этой цели.

 require 'minitest/spec' require 'minitest/autorun' require './student_file' describe StudentFile do describe "parse" do it "will return an instance of StudentFile" do StudentFile.parse('test_file.txt').must_be_kind_of(StudentFile) end end it "will return the correct class name" do valid_file.class_name.must_equal "Class Name" end it "will return true if the file is valid" do valid_file.valid_file?.must_be :==, true end it "will return false if the file is invalid" do invalid_file.valid_file?.must_be :==, false end end def valid_file StudentFile.new(["Class Namen", "RABBIT JESSICA 25FEMALEn", "001n"]) end def invalid_file StudentFile.new(["Class Name"]) end 

Таким образом, мы являемся частью пути туда. Мы установили, что должен быть parse метода уровня класса, который принимает имя файла и возвращает новый экземпляр StudentFile . Теперь мы можем проверить, что имя класса соответствует ожидаемому, и проверить количество строк контрольной суммы, ожидаемое в файле.

Общение с вашим кодом

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

Как мы проверяем вывод на консоли? Без сомнения, опытные профессионалы будут знать, но это заставило меня переосмыслить то, чего мы пытаемся достичь. Ранее я упоминал «выплюнуть на консоль», это был просто комментарий к временному выводу, который вызвал у нас небольшую головную боль. Однако, если мы сосредоточимся на том, чтобы просто выводить данные, а не где, мы начнем видеть прямое решение. Мы передаем вывод конструктору. StringIO справится с тестовыми задачами, мы можем установить настройки по умолчанию для консоли STDOUT и вуаля! у нас есть что-то более легкое для тестирования, но мы более гибкие.

 def self.parse(filename, output=STDOUT) f = File.new(filename) self.new(f.readlines, output) end 

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

 it "will display error if file is invalid" do out = StringIO.new invalid_file(out).print out.string.must_be :==, "INVALID FILE" end def invalid_file(op=STDOUT) StudentFile.new(["An invalid file"], op) end 

Просто издевайся

Прежде чем идти дальше, давайте кратко поговорим о насмешках и заглушках. При отправке проанализированного содержимого на консоль мы могли бы использовать среду для насмешек MiniTest или что-то вроде flexmock . Однако для этого примера я не чувствовал необходимости, так как StringIO будет выполнять эту работу так же адекватно. Кроме того, вывод находится в области того, что мы тестируем. Это действительно хорошая идея, чтобы издеваться над такими вещами?

Я склонен использовать издевательства только в нескольких ситуациях.

  1. Когда вы полагаетесь на ответы / поведение от третьей стороны (например, гем или веб-сервис)
  2. Опираясь на ответы / поведение фрагмента кода вне текущего домена. (например, обертка базы данных, скрипт электронной почты, который был протестирован в другом месте)
  3. Когда это позволяет мне сосредоточиться на элементе в сложном тесте.

Третья ситуация встречается не так часто. Однако, когда у нас есть кусок кода с большим количеством зависимостей, имеет смысл их смоделировать.

На с кодом

Теперь у нас есть способ проверить вывод на консоль. Прежде чем продолжить, давайте посмотрим на данные в файле. Моя первая реализация выглядела примерно так:

 def students row = [] @lines[1..-2].each do |line| row << ("| " << name(line) << "| " << age(line) << "| " << gender(line) << "|") end row.join("n") end def name(row) (row[14..27].strip << " " << row[0..13].strip).ljust(28) end def age(row) row[28..29].ljust(4) end def gender(row) row[30..-1].strip.ljust(13) end 

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

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

 describe StudentLine do before do @student = StudentLine.new("RABBIT JESSICA 25FEMALE") end it "will return the name of the student" do @student.full_name.musti_equal "JESSICA RABBIT" end it "will return the age of the student" do @student.age.must_equal "25" end it "should return the gender" do @student.gender.must_equal "FEMALE" end end 

StudentFile класс не только очищает реализацию класса StudentFile , но также изолирует тестовый код для файлов учащихся от входных и выходных данных синтаксического анализа данных учащихся из текстовой строки. Итак, если мы завернем печать данных закрытым методом (который технически не следует тестировать) примерно так:

 def initialize(lines, output=STDOUT) @class_name = lines.shift.strip @footer = lines.pop @lines = [] lines.each do |line| @lines << StudentLine.new(line) if line.length > 33 end @output = output end def print if self.valid_file? @output.print formatted_output else @output.print "INVALID FILE" end end private def formatted_output [class_name.upcase, separator, header, separator, students, separator].join("n") end def header "| STUDENT | AGE | GENDER |" end def separator "+-----------------------------+-----+--------------+" end def students row = [] @lines.each do |line| row << ("| " << line.full_name.ljust(28) << "| " << line.age.ljust(4) << "| " << line.gender.ljust(13) << "|") end row.join("n") end 

Завершение

У нас есть скрипт, который анализирует наш файл и выдает желаемые результаты. Вы можете просмотреть готовый источник в этой сути . Правда в том, что мы снова достигли стадии рефакторинга. Мы могли бы продолжить еще несколько действий и очистить код дальше. Однако мы не можем продолжать это делать. Мы должны подвести черту в какой-то момент, вместо того, чтобы помешаться на том, что наш код не так удивителен, как это может быть при продолжении рефакторинга. Это, вероятно, никогда не будет там. Однако у нас есть хорошо протестированный скрипт. Он делает то, что нам нужно, и в следующий раз мы можем позволить ему немного любви, именно это мы и сделаем. Наши тесты имеют нашу спину.

Надеемся, что эта статья показывает, что тесты не являются «приятными», они необходимы для любого проекта. В нашем распоряжении множество инструментов, и я настоятельно MiniTest::Spec вам попробовать MiniTest::Spec . Я приведу несколько советов для продуктивного тестирования:

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