Блог у синатри, епизода шеста

Пошто имам сада основну структуру, мало да “затегнем” тестирање.

Створим нови огранак у коме ћу да радим:

$ git checkout -b mocks

Кад сам намештао YAML објекат да врати садржај фајла:

YAML.stub!(:load_file).with("setup.yaml").and_return({"title" => "Naslov"})

ја сам користио stub!. А описивао сам stub. Иако обе методе врше исту сврху, stub! (са узвичником) потиче из RSpec верзије 1, док је stub из RSpec 2. Обе раде у верзији 2.

То може да се преправи, али би било боље да се пређе на нешто још и боље: mocks.

Пре него што то урадим да објасним шта је шта.

Stub је замена за објекат или делове, тј. методе, за објекат.

Mock је “лажни” објекат.

Која је разлика ?

Разлика између stub и mocks је у томе што је mock “интерактиван”. Шта то тачно значи? У овом случају то значи да mock јасно дефинише да треба да се назове/користи. Ако није назван/коришћен током теста, тест је неуспешан. Док stub неће да се жали ако се не користи у тесту.

Ево и примера:

describe ".title" do
  it "should return value" do
    YAML.stub!(:load_file).with("setup.yaml").and_return({"title" => "Naslov"}) 
    Config.title.should match("Naslov")
  end
end

ако додам у њега додам YAML.stub(:load):

describe ".title" do
  it "should return value" do
    YAML.stub!(:load_file).with("setup.yaml").and_return({"title" => "Naslov"}) 
    YAML.stub(:load) # <-- додатак
    Config.title.should match("Naslov")
  end
end

Тест ће да успе. Али ако то урадим са mock-ом:

describe ".title" do
  it "should return value" do
    YAML.stub!(:load_file).with("setup.yaml").and_return({"title" => "Naslov"}) 
    YAML.should_receive(:load)  # <-- додатак
    Config.title.should match("Naslov")
  end
end

тест не пролази:

Failures:

  1) Config.title should return value
     Failure/Error: YAML.should_receive(:load)
       (Syck).load(any args)
           expected: 1 time
           received: 0 times

Да би направили тест да буде што реалнији, боље би било да се пише овако:

describe ".title" do
  it "should return value" do
    YAML.should_receive(:load_file).with("setup.yaml").and_return({"title" => "Naslov"}) 
    Config.title.should match("Naslov")
  end
end

Онда заменим све stub! са should_receive и покренем тестове:

$ bundle exec rake 

сви пролазе. Да видим шта је измењено:

$ git status
# On branch mocks
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   spec/blurgh_spec.rb
#   modified:   spec/config_spec.rb

Онда да их додам у гит:

$ git add spec/blurgh_spec.rb spec/config_spec.rb
$ git commit -m "мала измена у тестовима, са stub на mock"

И онда додам нове измене у главно стабло кода:

$ git checkout master
Switched to branch 'master'
asimic@byzantium:~/dev/blurgh2$ git merge mocks
Updating 4f37cd4..c13635d
Fast-forward
 spec/blurgh_spec.rb |    2 +-
 spec/config_spec.rb |    6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

$ git merge mocks
Updating 4f37cd4..c13635d
Fast-forward
 spec/blurgh_spec.rb |    2 +-
 spec/config_spec.rb |    6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

Блог у синатри, епизода пета

До сада, радио сам само са тестовима, нисам заиста пробао апликацију. Најлакши начин да се покрене синатра апликација је користећи shotgun. Дакле:

$ git checkout -b proba
$ emacs Gemfile

И у њу додам:

gem 'shotgun'

Па $ bundle install

Да би покренуо апликацију:

$ shotgun blurgh.rb

И отворим http://localhost:9393/, али ту је грешка, јер нема setup.yaml фајла.

Направим setup.yaml са следећим садржајем:

title: Пази сад

Поново отворим страницу, и видим “Пази сад” у h1 таговима као што очекујем.

Да убацим садржај у гит:

$ git add Gemfile setup.yaml
$ git commit -m "апликација приказује странице"
$ git checkout master
$ git merge proba

Блог у синатри, епизода четврта

Пошто је ово систем за блог, било би пожељно да има начин како ће моћи да прикаже садржај.

Правим нови огранак:

$ git checkout -b views

Напишем следећи тест у blurgh_spec.rb:

context "the view" do
  it "should have a body html elements" do
    get '/'
    last_response.body.should match("<body>")
  end
end

следеће:

$ mkdir views
$ emacs views/layout.erb

у који додам:

<html>
  <body>
    <%= yield %>
  </body>
</html>

у фајл views/index.erb ставим:

<h1><%= @title %></h1>

У главном програму додам следеће:

get '/' do
  @title = Config.title
  erb :index 
end

покренем тест и он прође.

Онда рад додам у гит:

$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   blurgh.rb
#   modified:   spec/blurgh_spec.rb
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   views/
no changes added to commit (use "git add" and/or "git commit -a")

То ми говори да су измењена два фајла: blurgh.rb и spec/blurgh_spec.rb и додан views.

$ git add blurgh.rb spec/blurgh_spec.rb
asimic@byzantium:~/dev/blurgh2$ git add views/
asimic@byzantium:~/dev/blurgh2$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   blurgh.rb
#   modified:   spec/blurgh_spec.rb
#   new file:   views/index.erb
#   new file:   views/layout.erb

Друго додавање у гиту: git add views/, намерно сам користио / на самом крају јер то онда дода гиту све фајлове у том директоријому.

И за крај:

$ git commit -m "додани views"
$ git checkout master
$ git merge views

Блог у синатри, епизода трећа

А шта ако setup.yaml нема title дефинисан? Или ако нема setup.yaml ништа у себи?

Отворим нови огранак:

$ git checkout -b extra_checks

и ту почнем да пишем тестове у spec/config_spec.rb:

context "when the title is blank" do
  it "should return nothing" do
    YAML.stub!(:load_file).with("setup.yaml").and_return({"title" => ""}) 
    Config.title.should match("")
  end
end

Пробам:

$ bundle exec rspec spec/config_spec.rb

Тест прође. Онда додам други:

context "when the title is missing" do
  it "should return nil" do
    YAML.stub!(:load_file).with("setup.yaml").and_return({}) 
    Config.title.should be_nil
  end
end

И он прође. Онда пробам све тестове заједно:

$ bundle exec rake

Config
  .title
    should return value
  when the title is blank
    should return nothing
  when the title is missing
    should return nil

blurgh
  should respond to /
  should have a title

Finished in 0.01154 seconds
5 examples, 0 failures

Сви пролазе. Онда додам напредак у гит и све ставим у главно стабло:

$ git add spec/config_spec.rb
$ git commit -m "још тестова за Config.title" 
$ git checkout master
$ git merge extra_checks

Блог у синатри, епизода друга

Да би додали нешто ново, пожељно је да се направи нови огранак (branch) у гиту:

$ git checkout -b config_file    
$ git branch
* config_file
  master

Са задњом сам направио нови огранак и наместио њега као тренутно место у гиту где ће ићи сав нов рад.

Овај систем ће читати своју конфигурацију из setup.yaml фајла, чији формат ће изгледати овако:

title: Име мог блога
store: posts

Title ће бити сам наслов, suffix је завршни део назива фајлова, у овом случају у Markdown формату и store означава где ће се фајлови стављати.

Тако да када будем отишао на почетну страну ја требам да видим наслов или назив. То треба да се подеси у тесту:

it "should have a title" do
  get '/'
  last_response.body.should match("Naslov")
end

И ако покренем тестирање:

$ bundle exec rake

следећа грешка се појављује:

Failures:

  1) blurgh should have a title
     Failure/Error: last_response.body.should match("Naslov")
       expected "" to match "Naslov"
     # ./spec/blurgh_spec.rb:16:in `block (2 levels) in <top (required)>'

Да би то решио, ‘/’ адреса треба да испише наслов који ће да добије из фајла у YAML формату.

Пошто ништа не чита тај фајл, направићу модул који га чита. Али прво да кренем са тестом:

emacs spec/config_spec.rb

И ту опишем како би да се понаша тај модул:

require 'spec_helper'

describe "Config" do

  describe ".title" do
    it "should return value" do
      Config.title.should match("Naslov")
    end
  end

end  

Ту означим да метода title ће вратити вредност дефинисану у фајлу, у овом случају то ће бити “Naslov”.

Онда напишем Config модул у blurgh.rb:

module Config
  def self.title
    options["title"]
  end

  private
  def self.options
    YAML.load_file("setup.yaml")
  end
end

где приватна метода options чита YAML конфигурацију. Модул се

Да би могао да читам YAML формат, додам:

require 'yaml'

Вратим се тесту, покренем га, он не ради:

 Failure/Error: Config.title.should match("Naslov")
 Errno::ENOENT:
   No such file or directory - setup.yaml

То је зато што тај фајл и не постоји. Зато требa да се створи “лажни” фајл:

YAML.stub!(:load_file).with("setup.yaml").and_return({"title" => "Naslov"})

stub је део RSpec-а и он намести да метода врати пожељне резултате. У овом случају намести да YAML врати учитан фајл setup.yaml са садржајем “title: Naslov”.

Ако погледате пажљиво, видећете да он у ствари не вараћа

"title: Naslov" 

већ Hash са садржајем:

"title" => "Naslov"

То је зато што у овом случају ја желим да YAML објекат врати што “реалнији” резултат. Шта то значи? Када YAML учита неки фајл, он учита вредности из њега и стави их у Hash. А пошто ја овде хоћу да наместим YAML да ми врати пожељне вредности, ја их сам попуним.

Сада да пробам само задњи тест:

$ bundle exec rspec spec/config_spec.rb

Он прође.

Сада се вратим на првобитни тест у spec/blurgh_spec.rb и додам у it “should have a title” do:

YAML.stub!(:load_file).with("setup.yaml").and_return({"title" => "Naslov"}) 

покренем

$ bundle exec rake

и опет тест за наслов пада. То је зато што ништа се не враћа из главне синатрине методе. Да би то исправио додам следеће:

Config.title

покренем

$ bundle exec rake

И уместо да сви тестови прођу, први пада. То је првобитни тест, зато што он не може да нађе setup.yaml фајл. Да би то исправио, убацим следећи блок:

before :all do
  YAML.stub!(:load_file).with("setup.yaml").and_return({"title" => "Naslov"})     
end

Али тада само први тест прође, други пада. Ако изменим блок у before :each, онда сваки тест у blurgh_spec добије “лажни” фајл, то јест stub setup.yaml фајла:

require 'spec_helper'

describe "blurgh" do

  before :each do
    YAML.stub!(:load_file).with("setup.yaml").and_return({"title" => "Naslov"})     
  end

  def app
    @app ||= Sinatra::Application
  end

  it "should respond to /" do
    get '/'
    last_response.should be_ok
  end

  it "should have a title" do
    get '/'
    last_response.body.should match("Naslov")
  end

end


$ bundle exec rake

Config
  .title
    should return value

blurgh
  should respond to /
  should have a title

Finished in 0.01046 seconds
3 examples, 0 failures

После овог напредка, додам то у гит. Прво да видим статус:

$ git status
# On branch config_file
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   blurgh.rb
#   modified:   spec/blurgh_spec.rb
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   spec/config_spec.rb
no changes added to commit (use "git add" and/or "git commit -a")

Што значи да blurgh.rb и spec/blurgh_spec.rb су измењене а spec/config_spec.rb је нови фајл.

Све промене додам гит (у два корака):

$ git add blurgh.rb spec/blurgh_spec.rb spec/config_spec.rb
$ git status
# On branch config_file
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   blurgh.rb
#   modified:   spec/blurgh_spec.rb
#   new file:   spec/config_spec.rb
$ git commit -m "додан нови модул Config"

Погледао сам статус гита, чисто да видим да ли је све тачно унешено.

Тренутно сам и даље на огранку config_file, да потврдим:

$ git branch
\* config_file
  master

Да би унео нови део напредка у главно стабло гита, требам да се пребацим у њега и да упишем нове рад. То радим овако:

$ git checkout master
Switched to branch 'master'

$ git merge config_file
Updating b740f6e..b95240c
Fast-forward
 blurgh.rb           |   13 +++++++++++++
 spec/blurgh_spec.rb |   12 +++++++++++-
 spec/config_spec.rb |   12 ++++++++++++
 3 files changed, 36 insertions(+), 1 deletions(-)
 create mode 100644 spec/config_spec.rb

Блог систем у синатри, епизода прва

Идеја овог чланка је писање апликације у синатри, са тестирањем и гитом. Предходно знање ових технологија/апликација није потребно.

Блог систем треба да има YAML фајл у коме се убаци наслов, и место где ће да се држе чланци.

Од самог почетка користићу тестирање:

$ mkdir blurgh
$ cd blurgh 
$ emacs Gemfile

и додам:

source 'http://rubygems.org'

gem 'sinatra'
gem 'rack-test'
gem 'rspec'
gem 'rake'

онда инсталирам:

$ gem install -r bundle 
$ bundle install    

Да би омогућио да RSpec функционше, требам да направим следеће фајлове:

$ mkdir spec
$ emacs spec/spec_helper.rb

И додам следеће:

require File.join(File.dirname(__FILE__), '..', 'blurgh')

require 'sinatra'
require 'rack/test'
require 'rspec'


# set test environment
set :environment, :test
set :run, false
set :raise_errors, true
set :logging, false


RSpec.configure do |c|
  c.include Rack::Test::Methods
end    

Кључни део овде је додавање: Rack::Test::Methods. То омогућава тестирање уз помоћ rack.

Онда направим први тест:

$ emacs spec/blurgh_spec.rb

и напишем први тест:

require 'spec_helper'

describe "blurgh" do

  def app
    @app ||= Sinatra::Application
  end

  it "should respond to /" do
    get '/'
    last_response.should be_ok
  end
end

да покренем тај тест:

$ bundle exec rspec spec/blurgh_spec.rb

он наравно неће да прође јер још увек нема главне апликације, зато

$ emacs blurgh.rb

и додам следеће:

#!/usr/bin/env ruby

require 'sinatra'

get '/' do
end

поново покренем тестирање:

$ bundle exec rspec spec/blurgh_spec.rb

и избаци следеће резултате:

$ bundle exec rspec spec/blurgh_spec.rb 
.

Finished in 0.01479 seconds
1 example, 0 failures

да би то било мало разговетније, покренем тестирање са “документационом” опцијом и бојом, овако:

$ bundle exec rspec -f doc --colour spec/blurgh_spec.rb 

blurgh
  should respond to /

Finished in 0.01517 seconds
1 example, 0 failures

То је добар почетак, и то ћу да убацим у гит:

$ git init
$ emacs .gitignore

и ту ставим следеће:

Gemfile.lock

## Mac OS
.DS_Store

## Textmate
*.tmproj
tmtags

## Emacs
*~
\#*
.\#*

## Vim
*.swp

додам фајлове у гит:

$ git add .
$ git status

који врати следеће:

# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#   new file:   .gitignore
#   new file:   Gemfile
#   new file:   blurgh.rb
#   new file:   spec/blurgh_spec.rb
#   new file:   spec/spec_helper.rb
#

онда упишем те промене фајлове:

$ git commit -m "сам почетак"

што испише следеће резултате

[master (root-commit) ea5dd64] сам почетак
 5 files changed, 58 insertions(+), 0 deletions(-)
 create mode 100644 .gitignore
 create mode 100644 Gemfile
 create mode 100755 blurgh.rb
 create mode 100644 spec/blurgh_spec.rb
 create mode 100644 spec/spec_helper.rb

Али пошто је то досадно да се стално куца, користићу rake да то аутоматизујем:

$ emacs Rakefile

у коју додам ово:

require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new do |t|
  t.rspec_opts = ["-r ./spec/spec_helper.rb"]
  t.pattern = 'spec/**/*_spec.rb'
end

то омогућује RSpec да покреће тестове када се укуца:

$ bundle exec rake spec 

Али пошто ће то бити честа радња, да омогућим да куцам што мање, додајем и ово:

task :default => "spec"

Тако да могу да укуцам само:

$ bundle exec rake 

Да би имао боју и опис тестова, отворим нови фајл .rspec и у њега убацим:

--colour --format doc

Сада резултат изгледа боље:

$ bundle exec rake 

blurgh
  should respond to /

Finished in 0.0092 seconds
1 example, 0 failures

То сада може да се дода у гит:

$ git status

ми покаже који су нови фајлови које могу да додам:

$ git add Rakefile .rspec
$ git commit -m "додајем Rakefile и .rspec"

Опис књиге: Rails 3 in Action

Књиге које описују неку тренутну технологију или “алат” брзо остаре и њихов значај губи смисао, зато што касни за технологијом коју описује. Није много другачије што се тиче “Шина” (или ти Rails). Штавише, мислим да су књиге које описују Rails, кад се издају, већ “бајате” и говоре о томе како се радило јуче.

“Rails 3 in Action” покушава да то мало измени, тако што почиње да учи читаоца како се ради са Rails-ом почевши са методологијама где се пишу тестови прво, па тек онда сам програм. За то је аутор књиге изабрао Cucumber и RSpec. Све то је праћено коришћењем Git система за одржавање кода.

Аутор то ради тако што да мали увод у све технологије, али да би то најбоље дочарао прави апликацију и објашњава кораке. На тај начин доступа читаоцу не само да види како се тренутно пишу апликације са тестирањем, него како и он сам ради.

Моје разумевање “краставца” је да он треба да опише систем. Технологија коју он тестира може да се замени а да features и steps (делови који сачињавају краставац) могу да остану идентични. Начин на који аутор то приказује, није могуће. Модели скоро да нису тестирани, али зато контролери јесу.

Књига наводи два аутора: Јехуду Кетца и Рајана А. Бига. Али заправо Рајан је пише сам. Но то не значи ништа негативно, књига служи као добра референца.

Други разлог зашто наводим ауторе је JavaScript, и баш специфично jQuery. Иако је Јехуда један од главних програмера на Rails и jQuery пројектима (или барем је био до издања Rails верзије 3), jQuery се и не помиње. Има један део где се говори о Prototype, али то је само мали пример.

Да ли бих препоручио ову књигу? Моје мишљење је да не постоји један извор информација, без обзира на област. Тако да сматрам да је књига само још један начин на који можете видети нови приступ. Ја сам купио ову књигу, у MEAP* издању, и неки пут погледам како је аутор радио или применио нешто. Али читање блогова и праћење пројекта, не само Rails, већ Cucumber, RSpec и других је потребно. Али ако нисте пре користили Rails, и желите да научите како да развијате пројекат тако што пишете прво тестове па програм, књига је добар увод.

Rails 3 In Action - издавачева страница

Пројекат коришћен у књизи се налази овде на github-у.

* MEAP - електронско издање, где се добија приступ књизи док се још пише, поглавље по поглавље (Manning Early Access Program)

(текст је преправљан 13. априла 2011)

Tri saveta Rubi početniku

Pratim prilično redovno pitanja sa Ruby tagom na StackOverflow, i primetio sam da se dobar deo pitanja vezanih za Ruby kao jezik vrti oko načina za manipulaciju podacima smeštenim u osnovne Rubi strukture podataka - često postavljač pitanja okači svoje “naivno” parče koda i pita kako bi izgledalo “elegantnije” ili “rubističkije” rešenje.

S tim u vezi, evo par saveta za Rubi početnika koji želi da što pre prevaziđe ovakva pitanja, koja se zapravo svode na pisanje Java ili PHP koda u Rubiju:

  1. Naučite jako dobro čemu služe i kako rade metode Enumerable#map i Enumerable#inject!
  2. Prođite nekoliko puta kroz dokumentaciju za Enumerable, Array i Hash, pažljivo pročitajte koje metode nude i koje sve oblike te metode mogu da uzmu (neke primaju opcioni blok itd).
  3. Naučite šta su blokovi. Naučite u čemu se razlikuju od funkcija, kako se prosleđuju i pozivaju iz metoda. Naučite pojam closures.

Postoji još nekoliko “alata” koje Rubiju daju prednost nad “običnim” jezicima, ali se za početak zadržite na ove tri stavke, dok ih ne usvojite u potpunosti.

Je li f()==5 i 5==f() isto?

Vi koji ste radili sa C-olikim jezicima setićete se jedne od konvencija pri pisanju if-uslova, da se vrednost sa kojom se promenljiva poredi piše sa leve strane znaka jednakosti, a promenljiva s desne (if 5 == i naspram uobičajenog if i == 5), zbog česte greške da se umesto dvostrukog znaka jednakosti stavi jednostruki, i odradi dodela vrednosti promenljivoj s leve strane, za šta se neki kompajleri ne bi bunili.

Samo naoko sličan problem javlja se ponekad i u Rubiju, ali ne zbog moguće greške u kucanju, već zbog prirode znaka jednakosti.

Analizirajući performanse jedne skripte profajlerom, primetio sam da se u listi pozivanih metoda neočekivano javlja metoda Time#<=> (koja opet, iz ko zna kog razloga, zove BasicObject#method_missing), iako sam bio prilično siguran da u kodu nemam poređenje objekata klase Time. Detaljnijim pregledom shvatio sam da se poziv te metode javlja pri proveri rezultata jedne metode, tj. u otprilike ovakvom koodu:

return if m() == false

(Pre nego što počnete prozivku zbog onog == false, reći ću da mi je bila potrebna eksplicitna provera za false, tj. nisam hteo da mi se uslov ispuni ako je rezultat metode nil, već samo false.)

Ispostavilo se da metoda m u nekim slučajevima vraća objekat klase Time. (Ok, još malo pravdanja: metoda m je zapravo metoda send na jednom objektu, i može zapravo pozivati različite metode, otud nije baš uvek izvesno šta će se naći kao vraćena klasa.) S obzirom da je u Rubiju manje-više sve ili objekat ili poziv metode nekog objekta, ono == je takođe poziv metode. U slučaju klase Time, ovaj operator ne vrši jednostavno poređenje referenci, pa se usled toga == kome je poziv metode sa leve strane različito ponaša od onog gde je sa leve strane npr. false. Ok, da ne grešim dušu, kood se ponaša isto, ali se izvršava različitom brzinom, što opet nekad može da bude bitno.

Da bi se izbegla nepotrebna logika vezana za konkretnu implementaciju metode == levog objekta, dovoljno je promeniti redosled:

return if false == m()

false je u Rubiju instanca klase FalseClass, i njeno == je isto što i obično Object#==, odnosno jednostavna provera jesu li u pitanju isti objekti.