30 апреля 2009, 20:11
Тестирование Paperclip с помощью Factory Girl
Темы: ruby, rails, bdd
Введение
Регулярно в своей работе я встречаюсь с новыми инструментами и решаю разные задачи. Иногда новые решения кажутся незначительными и слишком простыми для того, чтобы посвящать им целый пост в блоге. Но в нашем блоггерском деле главное это регулярность. :)
Задача
Протестировать вновь изученный инструмент Paperclip с помощью вновь изученного инструмента Factory Girl.
Решение
Допустим, прикрепленный файл называется photo. То есть в модели написано следующее:
class Something < ActiveRecord::Base
has_attached_file :photo
end
Кладем файл с фото, например, в папку с fixtures. Тогда файл test/factories.rb (или spec/factories.rb) будет содержать:
Factory.define :something do |smth|
smth.photo File.new("path_to_file_in_fixtures", "rb")
end
И всё! Теперь в тестах, когда нужно создать объект пишем:
Factory.create(:something)
# или
Factory.build(:something)
А в тестах контроллера пишем:
Factory.attributes_for(:something)
Выводы
Если вы хотите написать проект за короткое ограниченное время, то лучше всего использовать инструменты, которые вы уже опробовали и знаете.
Если вы хотите изучить новые инструменты, то лучше всего сделать небольшой проектик с их использованием.
Проекты в первом и втором случае значительно отличаются по объему. :)
Материалы для изучения
Множество прекрасных инструментов от ThoughtBot
20 апреля 2009, 14:13
Тестирование OpenID с помощью Cucumber
Темы: ruby, rails, bdd
Задача
Какое-то время назад решил приобщиться к bdd (behavior driven development). С помощью такого подхода очень удобно описывать функционал приложения с точки зрения финального пользователя. И, соответственно, писать код так, чтобы выполнялись сценарии.
Для этого подхода существует прекрасный инструмент Cucumber, сценарии которого пишутся непосредственно текстом. Их, соответственно, можно даже обсуждать в качестве технического задания.
В качестве хорошего примера рассмотрим сценарий комментирования в этом блоге. Комментарии здесь можно оставлять только с помощью идентификации по OpenID.
Ресурсы
Нам понадобится, собственно, Cucumber, который для более лёгкого старта лучше использовать вместе с RSpec (фреймворк для удобного написания тестов и test driven development) и WebRat (инструмент для интегральных тестов, который содержит удобные методы работы с клиентским интерфейсом приложения).
Для работы с OpenID со стороны конечного пользователя нам понадобится локальный тестовый сервер OpenID. Есть хороший вариант: ROTS.
Затребуем библиотеки в тестовом окружении. В файле config/environments/test.rb:
config.gem "rspec", :lib => false, :version => ">=1.2.4"
config.gem "rspec-rails", :lib => false, :version => ">=1.2.4"
config.gem "webrat", :lib => false, :version => ">=0.4.4"
config.gem "cucumber", :lib => false, :version => ">=0.3.0"
config.gem "roman-rots", :lib => "rots", :version => ">=0.2.1"
Установим всё это:
sudo apt-get install libxml2-dev libxslt-dev
gem sources -a http://gems.github.com
rake gems:install RAILS_ENV=test
Решение
Предполагается, что как написать аутентификацию через OpenID мы знаем. Наша задача сейчас только протестировать функционал. В любом случае, когда мы приступаем к данному сценарию, у нас уже должны быть описаны и запрограммированы статьи и хотя бы создана модель для комментариев.
Теперь чтобы начать использовать Cucumber в приложении нужно запустить генератор:
script/generate cucumber
Процесс разработки в bdd выглядит так: сначала пишем сценарий, потом пишем всё остальное. Но в данном посте я просто покажу результат.
OpenID сервер
После установки библиотеки ROTS появляется консольная комманда:
rots
При запуске тестов необходимо, чтобы сервер был запущен. Для работы нам хватит параметров по умолчанию. В данном сервере уже есть пользователь, параметры для которого мы и будем использовать в тестах.
Сценарий Сucumber
Сценарии называются features. Создаем файл features/create_comments.feature:
Feature: Create Comments
In order to give feedback
As a reader
I want to create comments
Scenario: Create Comment
Given I visit a page for the published post with 1 approved comment
When I fill in "OpenID" with "http://localhost:1123/john.doe?openid.success=true"
And I fill in "Имя" with "John Doe"
And I fill in "Текст" with "Nice post!"
And I press "Отправить"
Then I should verify my OpenID
And I should see "John Doe"
And I should see "Nice post!"
And the post should have 2 approved comments
Многие из шагов уже определены с помощью WebRat и RSpec. Доопределим то, чего не хватает.
Шаги для статей
Тут есть моё внутреннее соглашение (с самим собой :). Когда я использую определенный артикль the, то имею в виду статью, которую я запоминаю в течение каждого сценария. В файле features/step_definitions/post_steps.rb:
Given /^I visit a page for the (published|draft) post with (\d+) (approved )?comments?$/ do |pub, cmnts_count, appr|
@post = Post.create!(:title => "Cucumber test post",
:body => "This is test post for cucumber comments",
:published => (pub == "published"))
cmnts_count.to_i.times do |cnt|
comment = @post.comments.build(:name => "Test User", :body => "Test body #{cnt}")
comment.openid_url = "http://test.example"
comment.approved = true unless appr.nil?
comment.save!
end
visit post_url(@post)
end
Then /^the post should have (\d+) (approved )?comments?$/ do |count, appr|
if appr.nil?
@post.comments.count.should == count.to_i
else
@post.approved_comments.count.should == count.to_i
end
end
Шаги для комментариев
Этот шаг специально написан для работы с ROTS, который, не требуя от пользователя никакого интерактивного взаимодействия в процессе теста отвечает на запрос редиректом подтверждая или отвергая авторизацию. В файле features/step_definitions/comment_steps.rb:
Then /^I should verify my OpenID$/ do
response = Net::HTTP.get_response(URI.parse(headers['location']))
response.class.should == Net::HTTPSeeOther
visit response['location']
end
Заключение
Вот и всё. Чтобы запустить тест, нужно выполнить:
cucumber features
Далее, конечно же, следует написать сценарии для пустых полей, для отказа в авторизации, для неправильного адреса OpenID и прочее. Но это, как мне кажется, не составляет труда.
Так же известно, что Cucumber поддерживает и русский язык. То есть сценарии можно писать и на русском. Но я пока не пробовал.
Материалы для изучения
RailsCast про авторизацию с помощью OpenID
RailsCast про использование Cucumber
Документация по Cucumber
14 апреля 2009, 16:30
Фильтрация rss-потоков с помощью Sinatra и HTTParty
Темы: ruby, rack, regexp, xml
Задача
Для фильтрации rss-потоков сужествует множество инструментов. Для своей задачи мне захотелось написать простейшее решение и заодно попробовать пару новых инструментов.
Надо: собрать воедино несколько единообразных rss-потоков, отфильтровав только нужное, и выдать единый rss-поток.
Для удобства предположим, что потоки имеют одинаковый формат atom. Адреса нужных нам потоков будут находиться в текстовом файле, разделенные переносом строки. Так же как и необходимые нам ключевые слова. Так же допустим, что наличие ключевых слов будем отслеживать в заголовках.
Ресурсы
Поскольку я собираюсь фильтровать на лету, мне не нужно ничего нигде хранить, я решил попробовать лёгкий руби-фреймворк под названием Sinatra.
А для работы с самими потоками, для получения их с их серверов используем простой и удобный инструмент HTTParty.
sudo gem i sinatra
sudo gem i httparty
Сбор и фильтрация
Создадим библиотечный файл feed_fetcher.rb:
require 'rubygems'
require 'httparty'
class FeedFetcher
include HTTParty
format :xml # позволяет получть результат сразу расфасованный
# в Hash
def self.get_items
urls = nil # будет массив адресов
titles = nil # будет массив нужных частей заголовков
items = [] # будет массив записей
File.open("path_to_feed_urls_file") do |f|
urls = f.readlines.each(&:strip!)
end
File.open("path_to_titles_file") do |f|
titles = f.readlines.each(&:strip!)
end
# составим единое регулярное выражение для фильтрации
retitles = Regexp.union(titles.reject(&:empty?).map { |t| %r{\b#{Regexp.escape(t)}\b}i })
# соберём записи со всех адресов в единый масив
urls.each do |u|
items += get(u)["rss"]["channel"]["item"] unless u.empty?
end
# отфильтруем по регулярному выражению и упорядочим по дате
items.select { |i| i["title"] =~ retitles }.sort do |x, y|
DateTime.parse(y["pubDate"]) <=> DateTime.parse(x["pubDate"])
end
end
end
Выдача результата
Результат будем так же выдавать в формате atom, поэтому нам понадобится builder, который, например, входит в состав active_support. Но можно установить его и отдельно.
Файл feed_filter.rb:
require 'rubygems'
require 'sinatra'
require 'active_support'
require 'feed_fetcher.rb'
get '/' do
content_type 'application/xml', :charset => 'utf-8'
@items = FeedFetcher.get_items
builder :index
end
По-умолчанию Sinatra хранит шаблоны в папке views. Файл views/index.builder:
xml.instruct!
xml.rss "version" => "2.0", "xmlns:atom" => "http://www.w3.org/2005/Atom" do
xml.channel do
xml.title "My Filtered Feed"
xml.link "http://lonelyelk.com"
xml.pubDate CGI::rfc1123_date Time.parse(@items.first["pubDate"]) if @items.any?
xml.description "Some description"
@items.each do |item|
xml.item do
item.each_pair do |key, value|
xml.tag!(key, value)
end
end
end
end
end
Запуск приложения с помощью passenger
Для запуска приложения мы будем использовать Passenger, который поддерживает не только rails, но и rack. Для этого нам понадобится создать папку public и указать к ней путь.
В установках виртуального сервера для apache:
<VirtualHost *:80>
ServerAdmin webmaster@mydomain.ru
ServerName feedfilter.mydomain.ru
DocumentRoot /path/to/feed_filter/public
...
</VirtualHost>
А в папке приложения нужно создать файл config.ru:
require 'rubygems'
require 'sinatra'
Sinatra::Application.set(:run, false)
Sinatra::Application.set(:environment, ENV['RACK_ENV'])
require 'feed_filter'
run Sinatra::Application
Вот и всё. Естественно, ещё следует написать тесты. Так же для публикации можно использовать capistrano. Но это, я думаю, всем под силу.
Материалы для изучения
Первое знакомство с фреймворком Sinatra