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