Статьи

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

Добро пожаловать! Если вы пропустили первую часть нашего путешествия , то, возможно, вы захотите сначала вернуться и наверстать упущенное.

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

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

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

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

Лучшее, что можно сделать в этой ситуации, — определить наш тест как «ожидающий», поэтому при каждом запуске тестовая среда будет игнорировать тест. Для этого вам нужно использовать ключевое слово pending :

1
2
3
4
5
describe «some method» do
  it «should do something»
    pending
  end
end

Все хорошие тестовые среды позволяют выполнять код до и после каждого теста. RSpec ничем не отличается.

Он предоставляет нам методы « before и « after которые позволяют нам настроить определенное состояние для запуска нашего теста, а затем очистить это состояние после выполнения теста (это так, чтобы состояние не утекло и не повлияло на результат последующие испытания).

01
02
03
04
05
06
07
08
09
10
11
12
13
describe «some method» do
  before(:each) do
    # some set-up code
  end
 
  after(:each) do
    # some tear-down code
  end
 
  it «should do something»
    pending
  end
end

Мы уже видели блок описаний; но есть другой блок, который функционально эквивалентен, называется context . Вы можете использовать его везде, где вы будете использовать, describe .

Разница между ними тонкая, но важная: context позволяет нам определить состояние для нашего теста. Хотя это и не явно (мы на самом деле не устанавливаем состояние, определяя блок context — оно вместо этого предназначено для удобства чтения, поэтому цель следующего кода более понятна).

Вот пример:

01
02
03
04
05
06
07
08
09
10
11
12
13
describe «Some method» do
  context «block provided» do
    it «yields to block» do
      pending
    end
  end
 
  context «no block provided» do
    it «calls a fallback method» do
      pending
    end
  end
end

Мы можем использовать метод- stub чтобы создать поддельную версию существующего объекта и вернуть ему заранее определенное значение.

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

Представьте, что у нас есть класс Person и у этого класса есть метод talk. Мы хотим проверить, работает ли этот метод так, как мы ожидаем. Чтобы сделать это, мы заглушим метод speak используя следующий код:

01
02
03
04
05
06
07
08
09
10
describe Person do
  it «speak()» do
    bob = stub()
    bob.stub(:speak).and_return(‘hello’)
    Person.any_instance.stub(:initialize).and_return(bob)
     
    instance = Person.new
    expect(instance.speak).to eq(‘hello’)
  end
end

В этом примере мы говорим, что «любой экземпляр» класса Person должен иметь свой метод initialize чтобы он возвращал объект bob .

Вы заметите, что bob сам по себе является заглушкой, которая настроена так, что любой временный код пытается выполнить метод talk, он возвращает «привет».

Затем мы приступаем к созданию нового экземпляра Person и передаем вызов instance.speak в синтаксис expect RSpec.

Мы сообщаем RSpec, что ожидаем, что вызов вызовет строку «привет».

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

Мы можем указывать разные возвращаемые значения каждый раз, когда вызывается заглушка, указав несколько аргументов для метода and_return :

1
2
3
4
5
6
obj = stub()
obj.stub(:foo).and_return(1, 2, 3)
 
expect(obj.foo()).to eq(1)
expect(obj.foo()).to eq(2)
expect(obj.foo()).to eq(3)

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

Для этого мы используем метод mock :

01
02
03
04
05
06
07
08
09
10
11
describe Obj do
  it «testing()» do
    bob = mock()
    bob.should_receive(:testing).with(‘content’)
 
    Obj.any_instance.stub(:initialize).and_return(bob)
 
    instance = Obj.new
    instance.testing(‘some value’)
  end
end

В приведенном выше примере мы создаем новый экземпляр Object и затем вызываем его метод testing .

За кулисами этого кода мы ожидаем, что метод testing будет вызван со значением 'content' . Если он не вызывается с этим значением (а в приведенном выше примере это не так), то мы знаем, что какой-то фрагмент нашего кода не функционировал должным образом.

Ключевое слово subject можно использовать несколькими способами. Все из которых предназначены для уменьшения дублирования кода.

Вы можете использовать его неявно (обратите внимание, что наш блок it вообще не ссылается на subject ):

1
2
3
4
5
6
describe Array do
  describe «with 3 items» do
    subject { [1,2,3] }
    it { should_not be_empty }
  end
end

Вы можете использовать его явно (обратите внимание, что наш блок it относится непосредственно к subject ):

1
2
3
4
5
6
7
8
9
describe MyClass do
  describe «initialization» do
    subject { MyClass }
    it «creates a new instance» do
      instance = subject.new
      expect(instance).to be_a(MyClass)
    end
  end
end

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
describe «Foo» do
  context «A» do
    it «Bar» do
      baz = Baz.new(‘a’)
      expect(baz.type).to eq(‘a’)
    end
  end
 
  context «B» do
    it «Bar» do
      baz = Baz.new(‘b’)
      expect(baz.type).to eq(‘b’)
    end
  end
 
  context «C» do
    it «Bar» do
      baz = Baz.new(‘c’)
      expect(baz.type).to eq(‘c’)
    end
  end
end

Вместо этого вы можете использовать subject вместе с let чтобы уменьшить дублирование:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
describe «Person» do
  subject { Person.new(name) } # Person has a get_name method
 
  context «Bob» do
    let(:name) { ‘Bob’ }
    its(:get_name) { should == ‘Bob’ }
  end
 
  context «Joe» do
    let(:name) { ‘Joe’ }
    its(:get_name) { should == ‘Joe’ }
  end
 
  context «Smith» do
    let(:name) { ‘Smith’ }
    its(:get_name) { should == ‘Smith’ }
  end
end

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

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

1
2
3
RSpec.configure do |config|
  config.order = ‘random’
end

Вы можете установить это с помощью команды, используя параметр --order flag /. Например: rspec --order random .

Когда вы используете --order random опцию —order, RSpec отобразит случайное число, которое использовалось для заполнения алгоритма. Вы можете снова использовать это «начальное» значение, если считаете, что обнаружили проблемы с зависимостями в своих тестах. Как только вы исправите то, что, по вашему мнению, является проблемой, вы можете передать начальное значение в RSpec (например, если начальное число было 1234 затем выполнить --order random:1234 ), и оно будет использовать то же самое рандомизированное начальное число, чтобы посмотреть, может ли оно реплицировать оригинальная ошибка зависимости.

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

Например, внутри .rspec :

1
—color —format nested

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

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

Но как инструмент отладки, Pry очень сфокусирован и прост.

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

В целях демонстрации Pry я собираюсь добавить больше кода в мой пример приложения (этот дополнительный код никак не влияет на наш тест)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class RSpecGreeter
  attr_accessor :test
 
  @@class_property = «I’m a class property»
 
  def greet
    binding.pry
    @instance_property = «I’m an instance property»
    pubs
    privs
    «Hello RSpec!»
  end
 
  def pubs
    test_var = «I’m a test variable»
    test_var
  end
 
  private
 
  def privs
    puts «I’m private»
  end
end

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

Наконец, вы заметите использование binding.pry .

Точка останова — это место в вашем коде, где выполнение остановится.

Вы можете установить несколько точек останова в своем коде и создавать их с помощью binding.pry .

Когда вы запустите свой код, вы заметите, что терминал остановится и поместит вас в код вашего приложения в том месте, где был установлен ваш binding.pry.

Ниже приведен пример того, как это может выглядеть …

1
2
3
4
5
6
8: def greet
=> 9: binding.pry
   10: pubs
   11: privs
   12: «Hello RSpec!»
   13: end

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

Вы можете запустить команду exit чтобы выйти из Pry и продолжить выполнение кода.

При использовании большого количества точек binding.pry может быть трудно понять, где в приложении вы находитесь.

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

При самостоятельном запуске вы увидите нечто похожее на то, как вы использовали binding.pry (вы увидите строку, на которой была установлена ​​точка останова, и пару строк выше и ниже этого). Разница в том, что если вы передадите дополнительный числовой аргумент, например, whereami 5 вы увидите пять дополнительных строк выше, где был размещен whereami 5 . Например, вы можете запросить 100 строк вокруг текущей точки останова.

Эта команда может помочь вам ориентироваться в текущем файле.

Команда wtf означает «что такое f ***» и предоставляет полную трассировку стека для самого последнего возникшего исключения. Это может помочь вам понять шаги, приводящие к появившейся ошибке.

Команда ls показывает, какие методы и свойства доступны для Pry.

При запуске он покажет вам что-то вроде …

1
2
3
RSpecGreeter#methods: greet pubs test test=
class variables: @@class_property
locals: _ __ _dir_ _ex_ _file_ _in_ _out_ _pry_

В приведенном выше примере мы видим, что у нас есть четыре открытых метода (помните, что мы обновили наш код, attr_accessor в него некоторые дополнительные методы, а затем были созданы test и test= при использовании сокращения Ruby attr_accessor ).

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

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

1
2
ls -p -G ^p
=> RSpecGreeter#methods: privs

В приведенном выше примере мы используем параметры / flags -p и -G , которые сообщают Pry, что мы хотим видеть только открытые и закрытые методы, и используем регулярное выражение ^p (что означает совпадение с любым, начинающимся с p ) в качестве шаблона поиска для фильтровать результаты.

Запуск ls --help также покажет вам все доступные опции.

Вы можете изменить текущую область с помощью команды cd .

В нашем примере, если мы запустим cd ../pubs это cd ../pubs нас к результату вызова этого метода.

Если мы сейчас запустим whereami вы увидите, что Inside "I'm a test variable" будет отображаться Inside "I'm a test variable" .

Если мы запустим self то увидим, что мы получили "I'm a test variable" .

Если мы запустим self.class мы увидим возвращенную String .

Вы можете перемещаться вверх по цепочке областей действия с помощью cd .. или вы можете вернуться на верхний уровень области действия с помощью cd / .

Примечание: мы могли бы добавить еще один binding.pry внутри метода pubs и тогда наша область действия была бы внутри этого метода, а не в результате метода.

Рассмотрим предыдущий пример работы cd pubs . Если мы запустим команду nesting мы увидим верхний уровень количества контекстов / уровней, которые в данный момент есть у Pry:

1
2
3
4
Nesting status:
0. # (Pry top level)
1. «I’m a test variable»

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

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

Если вы не уверены, где найти конкретный метод, вы можете использовать команду find-method чтобы показать вам все файлы в вашей базе кода, у которых есть метод, который соответствует тому, что вы ищете:

01
02
03
04
05
06
07
08
09
10
11
find-method priv
=> Kernel
   Kernel#private_methods
   Module
   Module#private_instance_methods
   Module#private_constant
   Module#private_method_defined?
   Module#private_class_method
   Module#private
   RSpecGreeter
   RSpecGreeter#privs

Вы также можете использовать опцию / -c для поиска содержимого файлов:

1
2
3
4
find-method -c greet
=> RSpecGreeter
   RSpecGreeter: def greet
   RSpecGreeter#privs: greet

Хотя описанные выше методы полезны, на самом деле это не «отладка» в том смысле, в котором вы, вероятно, привыкли.

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

Поскольку Pry разработан для использования в качестве REPL, это не значит, что он бесполезен для отладки.

Наивным решением было бы установить несколько операторов binding.pry через метод и использовать ctrl-d для перемещения по каждому набору точек останова. Но это все еще не достаточно хорошо.

Для пошаговой отладки вы можете загрузить gem pry-nav

01
02
03
04
05
06
07
08
09
10
11
12
13
14
source «https://rubygems.org»
 
gem ‘rspec’
 
group :development do
  gem ‘guard’
  gem ‘guard-rspec’
  gem ‘pry’
 
  # Adds debugging steps to Pry
  # continue, step, next
  gem ‘pry-remote’
  gem ‘pry-nav’
end

Этот драгоценный камень расширяет Pry, поэтому он понимает следующие команды:

  • Next (перейти к следующей строке)
  • Step (перейти к следующей строке и, если это метод, затем перейти к этому методу)
  • Continue (игнорировать дальнейшие точки останова в этом файле)

В качестве дополнительного бонуса давайте интегрируем наши тесты с онлайн-сервисом CI (непрерывная интеграция) Travis-CI .

Принцип CI заключается в том, чтобы фиксировать / выдвигать рано и часто, чтобы избежать конфликтов между вашим кодом и главной веткой. Когда вы это делаете (в данном случае мы подключаемся к GitHub), это должно запустить «сборку» на вашем CI-сервере, которая запускает соответствующие тесты, чтобы убедиться, что все работает так, как должно быть.

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

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

У многих компаний все еще есть больше сред, в которых их код должен пройти, прежде чем он достигнет рабочего сервера.

Например, на BBC News у нас есть:

  • CI
  • Тестовое задание
  • стадия
  • Прямой эфир

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

Travis CI — это служба непрерывной интеграции для сообщества открытого исходного кода. Он интегрирован с GitHub и предлагает первоклассную поддержку нескольких языков

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

Мы будем использовать бесплатную модель с открытым исходным кодом в нашем примере GitHub.

Процесс такой:

  • Зарегистрировать аккаунт на GitHub
  • Войдите в Travis-CI, используя свою учетную запись GitHub
  • Перейти на страницу « Аккаунты »
  • Включите все репозитории, на которых вы хотите запустить CI
  • Создайте файл .travis.yml в корневом каталоге вашего проекта и зафиксируйте его в своем репозитории GitHub.

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

Давайте посмотрим на файл .travis.yml который мы используем для нашего примера репозитория GitHub:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
language: ruby
cache: bundler
 
rvm:
  — 2.0.0
  — 1.9.3
 
script: ‘bundle exec rake spec’
 
bundler_args: —without development
 
branches:
  only:
    — master
 
notifications:
  email:
    — [email protected]

Давайте разберем это по частям …

Сначала мы указываем, какой язык мы используем в нашем проекте. В этом случае мы используем Ruby: language: ruby .

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

Travis-CI использует RVM (Ruby Version Manager) для установки Ruby на своих серверах. Поэтому нам нужно указать, с какими версиями Ruby мы хотим запускать наши тесты. В данном случае мы выбрали 2.0 и 1.9.3 которые являются двумя популярными версиями Ruby (технически наше приложение использует Ruby 2, но хорошо знать, что наш код передается и в других версиях Ruby):

1
2
3
rvm:
 — 2.0.0
 — 1.9.3

Для запуска наших тестов мы знаем, что можем использовать команду rake или rake spec . Travis-CI по умолчанию запускает команду rake но из-за того, как Gems устанавливаются на Travis-CI с помощью Bundler, нам нужно изменить команду по умолчанию: script: 'bundle exec rake spec' . Если бы мы этого не сделали, у Travis-CI возникла бы проблема с поиском rspec/core/rake_task который указан в нашем Rakefile .

Примечание: если у вас есть какие-либо проблемы, связанные с Travis-CI, вы можете присоединиться к каналу #travis на IRC-экране, чтобы получить помощь в ответе на любые ваши вопросы. Вот где я обнаружил решение моей проблемы с Travis-CI, которая не могла выполнять мои тесты с помощью команды rake умолчанию, и предложение перезаписать значение по умолчанию с помощью bundle exec rake решило эту проблему.

Далее, поскольку мы заинтересованы только в запуске наших тестов, мы можем передать дополнительные аргументы Travis-CI, чтобы отфильтровать гемы, которые мы не хотим беспокоить при установке. Поэтому для нас мы хотим исключить установку гемов, сгруппированных как разработка: bundler_args: --without development (это означает, что мы исключаем гемы, которые действительно используются только для разработки и отладки, такие как Pry и Guard).

Важно отметить, что изначально я загружал spec_helper.rb в нашем файле spec_helper.rb . Это вызвало проблему при запуске кода на Travis-CI, теперь я исключал гемы «разработки». Поэтому мне пришлось настроить код следующим образом:

1
require ‘pry’ if ENV[‘APP_ENV’] == ‘debug’

Вы можете видеть, что теперь гем APP_ENV require только в том случае, если для переменной среды APP_ENV установлено значение debug. Таким образом, мы можем избежать появления ошибок в Travis-CI. Это означает, что при локальном запуске кода вам нужно установить переменную окружения, если вы хотите отлаживать код с помощью Pry. Ниже показано, как это можно сделать в одну строку:

1
APP_ENV=debug && ruby lib/example.rb

Были внесены два других изменения, которые я внес в наш Gemfile . Один из них состоял в том, чтобы прояснить, какие гемы требовались для тестирования, а какие — для разработки, а другой был явно необходим Travis-CI:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
source «https://rubygems.org»
 
group :test do
  gem ‘rake’
  gem ‘rspec’
end
 
group :development do
  gem ‘guard’
  gem ‘guard-rspec’
  gem ‘pry’
 
  # Adds debugging steps to Pry
  # continue, step, next
  gem ‘pry-remote’
  gem ‘pry-nav’
end

Глядя на обновленный выше Gemfile мы видим, что мы переместили гем RSpec в новую группу test , так что теперь должно быть яснее, для чего предназначен каждый гем. Мы также добавили новый gem 'rake' . Документация Travis-CI утверждает, что это должно быть указано явно.

Следующий раздел является необязательным, и он позволяет вам помещать в белый (или черный список) определенные ветки в вашем хранилище. Поэтому по умолчанию Travis-CI будет запускать тесты для всех ваших веток, если вы не укажете обратное. В этом примере мы говорим, что хотим, чтобы он работал только с нашей master веткой:

1
2
3
branches:
 only:
   — master

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

1
2
3
branches:
 except:
   — some_branch_I_dont_want_run

Последний раздел сообщает Travis-CI, куда отправлять уведомления при сбое или успешной сборке:

1
2
3
notifications:
 email:

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

1
2
3
4
5
notifications:
 email:

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

1
2
3
4
5
6
notifications:
 email:
   recipients:
     — [email protected]
   on_failure: change
   on_success: never

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

Примечание: когда вы явно указываете on_failure или on_success вам необходимо переместить адреса электронной почты внутри ключа recipients .

На этом мы заканчиваем наши две части, посвященные RSpec, TDD и Pry.

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

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