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 для тестирования интерфейсов в веб-приложении.