Ruby — один из самых популярных языков, используемых в сети. Здесь мы проводим сессию на Nettuts +, которая познакомит вас с Ruby, а также с отличными фреймворками и инструментами, которые сопровождают разработку на Ruby. В этом выпуске вы узнаете о тестировании кода Ruby с помощью Rspec , одной из лучших библиотек для тестирования в бизнесе.
Предпочитаете скринкаст?
Выглядит знакомо?
Если вы читали мое недавнее руководство по JasmineJS , вы, вероятно, заметите несколько сходств в Rspec. На самом деле, сходство в Жасмин: Жасмин был создан с учетом Rspec. Мы рассмотрим, как можно использовать Rspec для создания TDD в Ruby. В этом уроке мы создадим несколько придуманных классов Ruby, чтобы познакомить нас с синтаксисом Rspec. Тем не менее, следующий эпизод «Ruby for Newbies» будет включать использование Rspec совместно с некоторыми другими библиотеками для тестирования веб-приложений… так что следите за обновлениями!
Настройка
Это довольно легко установить Rspec. Откройте эту командную строку и запустите это:
|
1
|
gem install rspec
|
Это просто.
Теперь давайте создадим небольшой проект. Мы собираемся создать два класса: Book и Library . Наши объекты Book будут хранить только название, автора и категорию. Наш объект Library будет хранить список книг, сохранять их в файл и позволять нам выбирать их по категориям.
Вот как должна выглядеть директория вашего проекта:

Мы помещаем спецификации (или спецификации) в папку spec ; у нас есть один файл спецификации для каждого класса. Обратите внимание на файл spec_helper.rb . Чтобы наши спецификации работали, нам нужны классы Ruby, которые мы тестируем. Вот что мы делаем внутри файла spec_helper :
|
1
2
3
4
|
require_relative ‘../library’
require_relative ‘../book’
require ‘yaml’
|
(Вы уже встречали require_relative ? Нет? Ну, require_relative — это то же require_relative , что и require , за исключением того, что вместо поиска по пути в Ruby он выполняет поиск относительно текущего каталога.)
Возможно, вы не знакомы с модулем YAML; YAML — это простая текстовая база данных, которую мы будем использовать для хранения данных. Вы увидите, как это работает, и мы поговорим об этом позже.
Итак, теперь, когда мы все настроены, давайте разберемся с некоторыми спецификациями!
Book класс
Давайте начнем с тестов для класса Book .
|
1
2
3
4
5
|
require ‘spec_helper’
describe Book do
end
|
Вот как мы начинаем: с блока describe . Наш параметр для describe объясняет то, что мы тестируем: это может быть строка, но в нашем случае мы используем имя класса.
Итак, что мы собираемся поместить в этот блок describe ?
|
1
2
3
|
before :each do
@book = Book.new «Title», «Author», :category
end
|
Мы начнем с того, что позвоним before ; мы передаем символ :each чтобы указать, что мы хотим, чтобы этот код запускался перед каждым тестом (мы также могли бы сделать :all чтобы запустить его один раз перед всеми тестами). Что именно мы делаем перед каждым тестом? Мы создаем экземпляр Book . Обратите внимание, как мы делаем его переменной экземпляра, добавляя имя переменной с @ . Мы должны сделать это, чтобы наша переменная была доступна из наших тестов. В противном случае мы просто получим локальную переменную, которая хороша только внутри блока before … что совсем не хорошо.
Двигаясь дальше,
|
1
2
3
4
5
|
describe «#new» do
it «takes three parameters and returns a Book object» do
@book.should be_an_instance_of Book
end
end
|
Вот наш первый тест. Мы используем здесь вложенный блок describe чтобы сказать, что мы описываем действия определенного метода. Вы заметите, что я использовал строку «#new»; в Ruby принято говорить, ссылаясь на методы экземпляров, как это: ClassName#methodName Так как у нас есть имя класса в нашем describe верхнего уровня, мы просто ClassName#methodName здесь имя метода.
Наш тест просто подтверждает, что мы действительно сделали объект Book.
Обратите внимание на грамматику, которую мы используем здесь: object.should do_something . Девяносто девять процентов ваших тестов примут эту форму: у вас есть объект, и вы начинаете с вызова should или не should_not на объекте. Затем вы передаете этому объекту вызов другой функции. В этом случае это be_an_instance_of (который принимает Book качестве единственного параметра). В целом, это делает отлично читаемый тест. Очень ясно, что @book должен быть экземпляром класса Book . Итак, давайте запустим это.
Откройте свой терминал, cd в каталог проекта и запустите rspec spec . spec — это папка, в которой rspec найдет тесты. Вы должны увидеть вывод, говорящий о «неинициализированной константе Object :: Book»; это просто означает, что нет класса Book . Давайте это исправим.
Согласно TDD, мы только хотим написать достаточно кода, чтобы решить эту проблему. В файле book.rb это будет так:
|
1
2
3
|
class Book
end
|
Перезапустите тест ( rspec spec ), и вы увидите, что он проходит нормально. У нас нет метода initialize , поэтому вызов Ruby#new сейчас не имеет никакого эффекта. Но мы можем создавать объекты Book (пусть и полые). Обычно мы следуем этому процессу до конца нашей разработки: пишем тест (или несколько связанных тестов), наблюдаем, как он проваливается, пропускаем, реорганизуем, повторяем. Однако для этого урока я просто покажу вам тесты и код и обсудим их.
Итак, еще тесты для Book :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
describe «#title» do
it «returns the correct title» do
@book.title.should eql «Title»
end
end
describe «#author» do
it «returns the correct author» do
@book.author.should eql «Author»
end
end
describe «#category» do
it «returns the correct category» do
@book.category.should eql :category
end
end
|
Все должно быть довольно просто для вас. Но обратите внимание, как мы сравниваем в тесте: с eql . Есть три способа проверить на равенство с Rspec: используя оператор == или метод eql оба возвращают true если два объекта имеют одинаковое содержимое. Например, оба являются строками или символами, которые говорят одно и то же. Тогда есть equal , которое возвращает true только в двух объектах, которые действительно и действительно равны, то есть они являются одним и тем же объектом в памяти. В нашем случае, eql (или == ) — это то, что мы хотим.
Они потерпят неудачу, поэтому вот код для Book чтобы заставить их пройти:
|
1
2
3
4
5
6
7
8
|
class Book
attr_accessor :title, :author, :category
def initialize title, author, category
@title = title
@author = author
@category = category
end
end
|
Давайте перейдем к Library !
Определение класса Library
Этот будет немного сложнее. Давайте начнем с этого:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
require ‘spec_helper’
describe «Library object» do
before :all do
lib_obj = [
Book.new(«JavaScript: The Good Parts», «Douglas Crockford», :development),
Book.new(«Designing with Web Standards», «Jeffrey Zeldman», :design),
Book.new(«Don’t Make me Think», «Steve Krug», :usability),
Book.new(«JavaScript Patterns», «Stoyan Stefanov», :development),
Book.new(«Responsive Web Design», «Ethan Marcotte», :design)
]
File.open «books.yml», «w» do |f|
f.write YAML::dump lib_obj
end
end
before :each do
@lib = Library.new «books.yml»
end
end
|
Все это настроено: мы используем два before блоком: один для :each и один для :all . В блоке before :all мы создаем массив книг. Затем мы открываем файл «books.yml» (в режиме «w») и используем YAML для выгрузки массива в файл.
Короткий кроличий след, чтобы объяснить YAML немного лучше: YAML, по мнению сайта, «дружественный человеку стандарт сериализации данных для всех языков программирования». Это похоже на текстовую базу данных, вроде JSON. Мы импортируем YAML в наш spec_helper.rb . Модуль YAML имеет два основных метода, которые вы будете использовать: dump , который выводит сериализованные данные в виде строки. Затем load принимает строку данных и передает ее обратно в объекты Ruby.
Итак, мы создали этот файл с некоторыми данными. Перед :each тесте мы собираемся создать объект Library , передав ему имя файла YAML. Теперь посмотрим на тесты:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
describe «#new» do
context «with no parameters» do
it «has no books» do
lib = Library.new
lib.should have(0).books
end
end
context «with a yaml file parameter» do
it «has five books» do
@lib.should have(5).books
end
end
end
it «returns all the books in a given category» do
@lib.get_books_in_category(:development).length.should == 2
end
it «accepts new books» do
@lib.add_book( Book.new(«Designing for the Web», «Mark Boulton», :design) )
@lib.get_book(«Designing for the Web»).should be_an_instance_of Book
end
it «saves the library» do
books = @lib.books.map { |book|
@lib.save
lib2 = Library.new «books.yml»
books2 = lib2.books.map { |book|
books.should eql books2
end
|
Мы начнем с внутреннего блока describe специально для метода Library#new . Здесь мы вводим еще один блок: context Это позволяет нам задавать контекст для тестов внутри него или определять различные результаты для различных ситуаций. В нашем примере у нас есть два разных контекста: «без параметров» и «с параметром файла yaml»; они показывают два поведения для использования Library#new .
Также обратите внимание на совпадения тестов, которые мы используем в этих двух тестах: lib.should have(0).books и @lib.should have(5).books . Другой способ написать это — lib.books.length.should == 5 , но это не так lib.books.length.should == 5 чтения. Тем не менее, это показывает, что нам нужно иметь свойство books которое является массивом имеющихся у нас книг.
Затем у нас есть три других теста, чтобы проверить функциональность получения книг по категориям, добавления книги в библиотеку и сохранения библиотеки. Все они терпят неудачу, поэтому давайте сейчас напишем класс.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
class Library
attr_accessor :books
def initialize lib_file = false
@lib_file = lib_file
@books = @lib_file ?
end
def get_books_in_category category
@books.select do |book|
book.category == category
end
end
def add_book book
@books.push book
end
def get_book title
@books.select do |book|
book.title == title
end.first
end
def save lib_file = false
@lib_file = lib_file ||
File.open @lib_file, «w» do |f|
f.write YAML::dump @books
end
end
end
|
Мы могли бы написать больше тестов и добавить множество других функций в этот класс Library , но на этом мы остановимся. Теперь запустив rspec spec , вы увидите, что все тесты пройдены.

Это не дает нам столько информации о тестах. Если вы хотите увидеть больше, используйте параметр вложенного формата: rspec spec --format nested . Вы увидите это:

Несколько последних матчей
Прежде чем мы подведем итоги, позвольте мне показать вам пару других матчей
-
obj.should be_true,obj.should be_false,obj.should be_nil,obj.should be_empty— первые три из них могут быть выполнены с помощью== trueи т. д.be_emptyбудет истинным, еслиobj.empty?правда. -
obj.should exist— этот объект еще существует? -
obj.should have_at_most(n).items,object.should have_at_least(n).items— аналогичноhave, но пройдет, если будет больше или меньшеnэлементов соответственно. -
obj.should include(a[,b,...])— один или несколько элементов в массиве? -
obj.should match(string_or_regex)— соответствует ли объект строке или регулярному выражению? -
obj.should raise_exception(error)— этот метод вызывает ошибку при вызове? -
obj.should respond_to(method_name)— этот объект имеет этот метод? Может принимать более одного имени метода в виде строк или символов.
Хотите узнать больше?
Rspec — одна из лучших платформ для тестирования в Ruby, и с ней можно многое сделать. Чтобы узнать больше, посетите веб-сайт Rspec . Есть также книга Rspec , которая учит больше, чем просто Rspec: все о TDD и BDD в Ruby. Я читаю это сейчас, и это очень тщательно и подробно.
Ну, вот и все для этого урока! В следующий раз мы рассмотрим, как мы можем использовать Rspec для тестирования интерфейсов в веб-приложении.