Статьи

Рубин для новичков: тестирование с Rspec

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 .

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 !


Этот будет немного сложнее. Давайте начнем с этого:

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