Статьи

Тестирование вашего кода Ruby с помощью Guard, RSpec & Pry

Моя недавняя работа была над облачным проектом Ruby для BBC News, предстоящих выборов 2014 года. Это требует быстрого ввода / вывода, масштабируемости и должно быть хорошо протестировано. Требование «быть хорошо проверенным» — это то, на чем я хочу сосредоточиться в этом уроке.

Этот проект использует несколько различных сервисов Amazon, таких как:

  • SQS (простая служба очереди)
  • DynamoDB (хранилище ключей / значений)
  • S3 (Простая служба хранения)

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

Хотя мы не будем использовать сервисы Amazon в этом учебнике, я упоминаю их, потому что для проведения быстрых тестов нам необходимо подделать эти внешние объекты (например, нам не нужно сетевое соединение для запуска нашего тесты, потому что эта зависимость может привести к медленному запуску тестов).

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

Я хочу поделиться с вами информацией об этих инструментах.

Инструменты, о которых я расскажу:

  • RSpec (среда тестирования)
  • Страж (бегун заданий)
  • Прай (REPL и отладка)

Я собираюсь сделать предположение, что вы знакомы с кодом Ruby и экосистемой Ruby. Например, мне не нужно объяснять вам, что такое «драгоценные камни» или как работают определенные синтаксис / концепции Ruby.

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

Возможно, вы не знакомы с Guard, но по сути это инструмент командной строки, который использует Ruby для обработки различных событий.

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

Это называется «бегущим заданием», вы, возможно, слышали эту фразу раньше, поскольку в настоящее время они широко используются в мире клиентской части ( Grunt и Gulp — два популярных примера).

Причина, по которой мы будем использовать Guard, заключается в том, что он помогает сделать цикл обратной связи (при выполнении TDD) намного более тесным. Это позволяет нам редактировать наши тестовые файлы, просматривать провальный тест, обновлять и сохранять наш код и сразу же проверять, прошел он или нет (в зависимости от того, что мы написали).

Вместо этого вы можете использовать что-то вроде Grunt или Gulp, но мы предпочитаем использовать такие типы задач для обработки внешнего интерфейса и клиентской части. Для внутреннего / серверного кода мы используем Rake и Guard.

RSpec, если вы еще не знали, это инструмент тестирования для языка программирования Ruby .

Вы запускаете свои тесты (используя RSpec) через командную строку, и я покажу, как вы можете упростить этот процесс с помощью программы сборки Ruby, Rake .

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

Хотя это и необязательно для демонстрации использования RSpec и Guard, стоит отметить, что я полностью поддерживаю использование TDD в качестве средства обеспечения того, чтобы каждая строка кода, которую вы пишете, имела цель и была разработана тестируемым и надежным способом.

Я подробно опишу, как мы будем делать TDD с простым приложением, чтобы вы хотя бы почувствовали, как работает этот процесс.

Я создал базовый пример на GitHub, чтобы избавить вас от необходимости печатать все самостоятельно. Не стесняйтесь скачать код .

Давайте теперь продолжим и рассмотрим этот проект, шаг за шагом.

Для работы нашего примера приложения требуются три основных файла:

  1. Gemfile
  2. Guardfile
  3. Rakefile

Мы кратко рассмотрим содержимое каждого файла, но первое, что нам нужно сделать, — это создать структуру каталогов.

Для нашего примера проекта нам понадобятся две папки:

  • lib (будет содержать код нашего приложения)
  • spec (это будет содержать наш тестовый код)

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

Откройте свой терминал и выполните следующую команду:

1
gem install bundler

Bundler — это инструмент, который облегчает установку других драгоценных камней.

Gemfile эту команду, создайте три вышеуказанных файла ( Gemfile , Guardfile и Rakefile ).

Gemfile отвечает за определение списка зависимостей для нашего приложения.

Вот как это выглядит:

1
2
3
4
5
6
7
8
9
source «https://rubygems.org»
 
gem ‘rspec’
 
group :development do
  gem ‘guard’
  gem ‘guard-rspec’
  gem ‘pry’
end

Как только этот файл будет сохранен, запустите команду bundle install .

Это установит все наши драгоценные камни для нас (включая те драгоценные камни, которые указаны в группе development ).

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

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

Чтобы установить соответствующие гемы на вашем производственном сервере, вам нужно запустить что-то вроде:

1
bundle install —without development

Rakefile позволит нам запускать наши тесты RSpec из командной строки.

Вот как это выглядит:

1
2
3
4
5
require ‘rspec/core/rake_task’
 
RSpec::Core::RakeTask.new do |task|
  task.rspec_opts = [‘—color’, ‘—format’, ‘doc’]
end

Примечание : вам не нужен Guard, чтобы иметь возможность запускать ваши тесты RSpec. Мы используем Guard, чтобы сделать TDD проще.

Когда вы устанавливаете RSpec, он дает вам доступ к встроенной задаче Rake, и это то, что мы используем здесь.

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

В этом случае мы хотим, чтобы вывод нашей оболочки был цветным, и мы хотим отформатировать вывод в стиле doc (вы можете изменить формат для nested в качестве примера).

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

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

1
rake spec

Это дает нам следующий вывод:

1
2
3
4
5
6
7
8
rake spec
/bin/ruby -S rspec ./spec/example_spec.rb —color —format doc
 
RSpecGreeter
  RSpecGreeter#greet()
 
Finished in 0.0006 seconds
1 example, 0 failures

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

Содержимое этого файла говорит Guard, что делать, когда мы запускаем команду guard :

01
02
03
04
05
06
07
08
09
10
11
guard ‘rspec’ do
  # watch /lib/ files
  watch(%r{^lib/(.+).rb$}) do |m|
    «spec/#{m[1]}_spec.rb»
  end
 
# watch /spec/ files
  watch(%r{^spec/(.+).rb$}) do |m|
    «spec/#{m[1]}.rb»
  end
end

Вы Gemfile что в нашем Gemfile мы указали gem: guard-rspec . Нам нужен этот драгоценный камень, чтобы Guard мог понять, как обрабатывать изменения в файлах, связанных с RSpec.

Если мы снова посмотрим на содержимое, мы увидим, что если мы guard rspec то Guard будет наблюдать за указанными файлами и выполнять указанные команды, как только произойдут какие-либо изменения в этих файлах.

Примечание : поскольку у нас есть только одна охранная задача rspec , то она запускается по умолчанию, если мы запустили команду guard .

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

В этом случае мы просим Guard следить за всеми файлами в наших папках lib и spec и, если какие-либо изменения происходят с любым из этих файлов, затем выполнить тестовые файлы в нашей папке spec чтобы убедиться, что никакие изменения, которые мы сделали, не сломали наши тесты. (и впоследствии не нарушил наш код).

Если у вас есть все файлы, загруженные из репозитория GitHub, вы можете попробовать эту команду для себя.

Запустите guard и сохраните один из файлов, чтобы увидеть, как он запускает тесты.

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

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

Давайте теперь рассмотрим пример спецификации (например, наш тестовый файл), в которой будут описаны наши требования. После этого мы начнем шагать по коду, определенному в спецификации, и посмотрим, как мы можем использовать TDD, чтобы помочь нам в написании нашего приложения.

Мы собираемся создать файл с названием example_spec.rb . Цель этого файла — стать нашим файлом спецификации (другими словами, это будет наш тестовый код и будет представлять ожидаемую функциональность).

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

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

Как правило, если вы сначала пишете код своего приложения (то есть вы не используете TDD), то вы обнаружите, что пишете код, который в какой-то момент в будущем будет чрезмерно спроектирован и потенциально устарел. В процессе рефакторинга или изменения требований вы можете обнаружить, что некоторые функции никогда не будут вызываться.

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

Вот наш тестовый код:

1
2
3
4
5
6
7
8
9
require ‘spec_helper’
 
describe ‘RSpecGreeter’ do
  it ‘RSpecGreeter#greet()’ do
    greeter = RSpecGreeter.new # Given
    greeting = greeter.greet # When
    greeting.should eq(‘Hello RSpec!’) # Then
  end
end

Вы можете заметить комментарии к коду в конце каждой строки:

  • Given
  • When
  • Then

Это форма терминологии BDD (Behavior-Driven Development). Я включил их для читателей, которые более знакомы с BDD (Behavior-Driven Development) и которые интересовались, как они могут приравнять эти утверждения к TDD.

Первое, что мы делаем внутри этого файла, это загружаем spec_helper.rb (который находится в том же каталоге, что и наш spec-файл). Мы вернемся и посмотрим на содержимое этого файла через минуту.

Далее у нас есть два кодовых блока, специфичных для RSpec:

  • describe 'x' do
  • it 'y' do

Первый блок описаний должен адекватно описывать конкретный класс / модуль, над которым мы работаем и предоставляем тесты. Вы вполне можете иметь несколько блоков describe в одном файле спецификации.

Существует множество различных теорий о том, как использовать блоки description и it description. Я лично предпочитаю простоту, поэтому я буду использовать идентификаторы для класса / модулей / методов, которые мы будем тестировать. Но вы часто встречаете людей, которые предпочитают использовать полные предложения для своих описаний. Ни правильно, ни неправильно, просто личные предпочтения.

Блок it отличается и всегда должен быть помещен внутри блока describe . Это должно объяснить, как мы хотим, чтобы наше приложение работало.

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

Содержимое блока it — это код, который мы собираемся протестировать.

В приведенном выше примере мы создаем новый экземпляр класса RSpecGreeter (который еще не существует). Мы отправляем сообщение greet (которое также еще не существует) созданному объекту ( примечание : на данный момент эти две строки являются стандартным кодом Ruby).

Наконец, мы сообщаем платформе тестирования, что ожидаем, что результатом вызова метода greet будет текст « Hello RSpec! » С использованием синтаксиса RSpec: eq(something) .

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

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

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

Чтобы помочь нам сократить стандартный код, мы поместим его в специальный вспомогательный файл, который мы загрузим из наших файлов спецификаций. Этот файл будет называться spec_helper.rb .

Этот файл будет делать пару вещей:

  • сообщите Ruby, где находится наш основной код приложения
  • загрузить код нашего приложения (для запуска тестов)
  • загрузить pry gem (помогает нам отлаживать наш код; если нам нужно).

Вот код:

1
2
3
4
$ << File.join(File.dirname(FILE), ‘..’, ‘lib’)
 
require ‘pry’
require ‘example’

Примечание : первая строка может выглядеть немного загадочно, поэтому позвольте мне объяснить, как это работает. Здесь мы говорим, что хотим добавить папку /lib/ в системную переменную Ruby $LOAD_PATH . Всякий раз, когда Ruby выполняет оценку, require 'some_file' него есть список каталогов, в которых он пытается найти этот файл. В этом случае мы проверяем, что если у нас есть код require 'example' , Ruby сможет найти его, потому что он проверит наш каталог /lib/ и там найдет указанный файл. Это хитрый трюк, который вы увидите во многих драгоценных камнях Ruby, но он может быть довольно запутанным, если вы никогда не видели его раньше.

Наш код приложения будет находиться внутри файла с названием example.rb .

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

Начнем с предположения, что вы используете guard для запуска своих тестов (поэтому каждый раз, когда мы вносим изменения в example.rb , Guard замечает это изменение и переходит к запуску example_spec.rb чтобы убедиться, что наши тесты пройдены).

Чтобы мы правильно сделали TDD, наш файл example.rb будет пустым, и поэтому, если мы откроем файл и «сохраним» его в текущем состоянии, то Guard запустится, и мы обнаружим (неудивительно), что наш тест не пройден:

01
02
03
04
05
06
07
08
09
10
11
12
13
Failures:
  1) RSpecGreeter RSpecGreeter#greet()
     Failure/Error: greeter = RSpecGreeter.new # Given
     NameError:
       uninitialized constant RSpecGreeter
     # ./spec/example_spec.rb:5:in</code>block (2 levels) in ‘
  
Finished in 0.00059 seconds
1 example, 1 failure
  
Failed examples:
  
rspec ./spec/example_spec.rb:4 # RSpecGreeter RSpecGreeter#greet()

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

Смысл TDD в том, чтобы иметь тесную петлю обратной связи , также известную как «красный, зеленый, рефакторинг»). Что это означает на практике:

  • написать провальный тест
  • напишите наименьшее количество кода, чтобы заставить его пройти
  • рефакторинг кода

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


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

1
2
3
class RSpecGreeter
  # code will eventually go here
end

Это приведет к следующей ошибке:

1
2
3
4
5
6
7
8
9
Failures:
  1) RSpecGreeter RSpecGreeter#greet()
     Failure/Error: greeter = greeter.greet # When
     NoMethodError:
       undefined methodgreet’ for #
     # ./spec/example_spec.rb:6:in `block (2 levels) in ‘
 
Finished in 0.00036 seconds
1 example, 1 failure

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

1
2
3
4
5
class RSpecGreeter
  def greet
    # code will eventually go here
  end
end

ОК, мы почти у цели. Ошибка, которую мы получаем сейчас:

01
02
03
04
05
06
07
08
09
10
11
12
Failures:
  1) RSpecGreeter RSpecGreeter#greet()
     Failure/Error: greeter = greeting.should eq(‘Hello RSpec!’) # Then
 
       expected: «Hello RSpec!»
            got: nil
 
       (compared using ==)
     # ./spec/example_spec.rb:7:in `block (2 levels) in ‘
 
Finished in 0.00067 seconds
1 example, 1 failure

RSpec сообщает нам, что ожидал увидеть Hello RSpec! но вместо этого он получил nil (потому что мы определили метод greet но на самом деле ничего не определяли внутри метода и поэтому он возвращает nil ).

Мы добавим оставшуюся часть кода, которая позволит пройти наш тест:

1
2
3
4
5
class RSpecGreeter
  def greet
    «Hello RSpec!»
  end
end

Вот оно, прохождение теста:

1
2
Finished in 0.00061 seconds
1 example, 0 failures

Мы сделали здесь. Наш тест написан и код проходит.

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

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