Моя недавняя работа была над облачным проектом Ruby для BBC News, предстоящих выборов 2014 года. Это требует быстрого ввода / вывода, масштабируемости и должно быть хорошо протестировано. Требование «быть хорошо проверенным» — это то, на чем я хочу сосредоточиться в этом уроке.
Вступление
Этот проект использует несколько различных сервисов Amazon, таких как:
- SQS (простая служба очереди)
- DynamoDB (хранилище ключей / значений)
- S3 (Простая служба хранения)
Нам нужно уметь писать быстрые тесты и мгновенно сообщать нам о проблемах с нашим кодом.
Хотя мы не будем использовать сервисы Amazon в этом учебнике, я упоминаю их, потому что для проведения быстрых тестов нам необходимо подделать эти внешние объекты (например, нам не нужно сетевое соединение для запуска нашего тесты, потому что эта зависимость может привести к медленному запуску тестов).
Помимо технологического лидера Роберта Кенни (который очень хорошо разбирается в написании приложений Ruby на основе TDD (разработка на основе тестирования) ), мы использовали различные инструменты, которые сделали этот процесс и наше программирование намного проще.
Я хочу поделиться с вами информацией об этих инструментах.
Инструменты, о которых я расскажу:
Что мне нужно знать заранее?
Я собираюсь сделать предположение, что вы знакомы с кодом Ruby и экосистемой Ruby. Например, мне не нужно объяснять вам, что такое «драгоценные камни» или как работают определенные синтаксис / концепции Ruby.
Если вы не уверены, то перед тем, как двигаться дальше, я бы порекомендовал прочитать одну из моих других публикаций на Ruby, чтобы освоиться.
охрана
Возможно, вы не знакомы с Guard, но по сути это инструмент командной строки, который использует Ruby для обработки различных событий.
Например, Guard может уведомлять вас о редактировании определенных файлов, а вы можете выполнять некоторые действия в зависимости от типа файла или события, которое было сгенерировано.
Это называется «бегущим заданием», вы, возможно, слышали эту фразу раньше, поскольку в настоящее время они широко используются в мире клиентской части ( Grunt и Gulp — два популярных примера).
Причина, по которой мы будем использовать Guard, заключается в том, что он помогает сделать цикл обратной связи (при выполнении TDD) намного более тесным. Это позволяет нам редактировать наши тестовые файлы, просматривать провальный тест, обновлять и сохранять наш код и сразу же проверять, прошел он или нет (в зависимости от того, что мы написали).
Вместо этого вы можете использовать что-то вроде Grunt или Gulp, но мы предпочитаем использовать такие типы задач для обработки внешнего интерфейса и клиентской части. Для внутреннего / серверного кода мы используем Rake и Guard.
RSpec
RSpec, если вы еще не знали, это инструмент тестирования для языка программирования Ruby .
Вы запускаете свои тесты (используя RSpec) через командную строку, и я покажу, как вы можете упростить этот процесс с помощью программы сборки Ruby, Rake .
подглядывать
Наконец, мы будем использовать еще один Ruby-гем под названием Pry, который является чрезвычайно мощным инструментом отладки Ruby, который внедряется в ваше приложение во время его работы, чтобы вы могли проверить свой код и выяснить, почему что-то не работает.
TDD (разработка через тестирование)
Хотя это и необязательно для демонстрации использования RSpec и Guard, стоит отметить, что я полностью поддерживаю использование TDD в качестве средства обеспечения того, чтобы каждая строка кода, которую вы пишете, имела цель и была разработана тестируемым и надежным способом.
Я подробно опишу, как мы будем делать TDD с простым приложением, чтобы вы хотя бы почувствовали, как работает этот процесс.
Создание примера проекта
Я создал базовый пример на GitHub, чтобы избавить вас от необходимости печатать все самостоятельно. Не стесняйтесь скачать код .
Давайте теперь продолжим и рассмотрим этот проект, шаг за шагом.
Основные файлы
Для работы нашего примера приложения требуются три основных файла:
Gemfile
-
Guardfile
-
Rakefile
Мы кратко рассмотрим содержимое каждого файла, но первое, что нам нужно сделать, — это создать структуру каталогов.
Структура каталогов
Для нашего примера проекта нам понадобятся две папки:
-
lib
(будет содержать код нашего приложения) -
spec
(это будет содержать наш тестовый код)
Это не является обязательным требованием для вашего приложения, вы можете легко настроить код в других наших файлах для работы с любой структурой, которая вам подходит.
Установка
Откройте свой терминал и выполните следующую команду:
1
|
gem install bundler
|
Bundler — это инструмент, который облегчает установку других драгоценных камней.
Gemfile
эту команду, создайте три вышеуказанных файла ( Gemfile
, Guardfile
и Rakefile
).
Gemfile
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
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
|
Как видите, отказов нет. Это потому, что, хотя у нас нет написанного кода приложения, у нас также еще нет написанного тестового кода.
Guardfile
Содержимое этого файла говорит 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, чтобы помочь вам отладить и написать свой код.