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

Било би пожељно да овај систем може да учита чланке и да их прикаже. Идеја је да чита чланке из одређеног директоријума који је наведен у конфигурационом фајлу setup.yaml на следећи начин:

store: posts

За то да направим нови огранак у гиту:

$ git checkout -b posts

Прво желим да Config чита и .store врати име директоријума. То означим у тесту овако:

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

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

1) Config.store should return value
   Failure/Error: Config.store.should match("posts")
   NoMethodError:
     undefined method `match' for nil:NilClass

Онда у Config додам нову методу:

def self.store
  options["store"]
end

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

$ git status
# On branch posts
# 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/config_spec.rb
#
no changes added to commit (use "git add" and/or "git commit -a")
$ git add blurgh.rb spec/config_spec.rb
$ git commit -m "додана метода Config.store"
[posts b243446] додана метода Config.store
 2 files changed, 12 insertions(+), 0 deletions(-)

Сада имам тачно одређено место где ће да се држе чланци, али још немам ништа да их учитам. То желим да ми уради метода get_posts која ће да врати све фајлове директоријума на који указује store. Тест за ту методу изгледа овако:

describe "get_posts" do
  it "should return posts" do
    Dir.should_receive(:entries).with("posts").and_return(["sample_post.md", "another_sample_post.md"])
    get_posts.should == ["sample_post.md", "another_sample_post.md"]
  end
end

И кад покренем тестове, добијем следеће:

 1) blurgh get_posts should return posts
Failure/Error: get_posts.should == ["sample_post.md", "another_sample_post.md"]
NameError:
  undefined local variable or method `get_posts' for #<RSpec::Core::ExampleGroup::Nested_2::Nested_2:0x00000001f12da8>

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

def get_posts
  Dir.entries(Config.store)
end

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

Но то ми није довољно, јер како ћу да организујем чланке, овако су враћени у “хрпи”. Било би боље када би они могли да буду организовани по датуму.

И како ћу за наслов ?

То би могло да се реши овако: сваки чланак да буде сачињен делимично у YAML формату, то јест, први параграф, а остатак да буде сам чланак. На пример да садржај фајла изгледа овако:

title: Кападокија
date: 20110324

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

То би било у реду за један чланак, али шта са get_posts методом ? У каквом формату да врати чланке ?

Било би добро да буду враћени у следећем формату:

[[["20110324"], {"title" => "Кападокија", "body" => "Премало се зна о Кападокији..."}]]

На тај начин могу лако да сортирам чланке.

И шта још би могао да ту додам? Адресу, тј. URL сваког чланка. Намерно бих да title буде наслов у самој страници а да име фајла буде URL. Тако да ако предходни пример чланак назовем: o-kapadokji.md, формат који би get_posts метода враћала би био:

[["20110324", {"url" => "o-kapadokiji", "title" => "Кападокија", "body" => "Премало се зна о Кападокији..."}]]

Да то направим да буде направићу два фајла која ћу да користим за тестирање:

$ mkdir spec/fixtures
$ emacs spec/fixtures/o-kapadokiji.md 

И ту ставим горе наведени текст. Онда отворим други фајл:

$ emacs spec/fixtures/let.md

Ставим следећи текст:

title: Авионски лет
date: 20110325

25. марта 1923. - Слетањем авиона на линији Париз-Цариград на аеродром
у Панчеву, Краљевина СХС постала део тада малобројне породице држава
повезаних ваздушним саобраћајем.

Сада да преправим тест:

describe "get_posts" do
  it "should return posts" do
    first_article = File.readlines("spec/fixtures/o-kapadokiji.md", "")[1]
    second_article = File.readlines("spec/fixtures/let.md", "")[1]
    YAML.should_receive(:load_file).with("setup.yaml").and_return({"title" => "Naslov", "store" => "spec/fixtures"}) 

    get_posts.should == \
    [[20110325,  {"url" => "let", "title" => "Авионски лет", "body" => "#{second_article}"}],\
     [20110324, {"url" => "o-kapadokiji", "title" => "Кападокија", "body" => "#{first_article}"}]]

  end
end

Да објасним овде да first_article и second_article су само фајлови учитани, да не бих писао цео њихов садржај. И store сада чита фајлове из spec/fixtures.

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

$ bundle exec rspec spec/blurgh_spec.rb

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

1) blurgh get_posts should return posts
     Failure/Error: get_posts.should == \
     Errno::ENOENT:
       No such file or directory - posts

То је зато што на самом почетку теста имам:

before :each do
  YAML.should_receive(:load_file).with("setup.yaml").and_return({"title" => "Naslov", "store" => "posts"}) 
end

који попуњава store са posts. Да би то спречио одвојићу прва неколико теста у једну целину на следећи начин:

 context "routes and content" do
   before :each do
     YAML.should_receive(:load_file).with("setup.yaml").and_return({"title" => "Naslov", "store" => "posts"}) 
   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

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

док је други тест сам за себе.

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

1) blurgh get_posts should return posts
   Failure/Error: [["20110325"], {"url" => "let", "title" => "Авионски лет", "body" => "#{second_article}"}]]
     expected: [[["20110324"], {"url"=>"o-kapadokiji", "title"=>"Кападокија",
     ...

што је и пожељан резултат. Јер ако се анлизира грешка, драгачија је од предходне: не жали се о томе како не може да прочита неки фајл, већ да резултат методе није идентичан очекиваном.

Сада да прилагодим методу, да би прошао тест:

def get_posts

  all_posts = Hash.new {
    |h,k| h[k] = Hash.new(&h.default_proc) 
  }

  post_dir = File.join(Config.store + "/" + "*.md")

  Dir.glob(post_dir).each do |post|
    header, body = File.readlines(post, "")
    data = YAML.load(header)
    all_posts[data['date']]['url']   = post.gsub("\.md", "").gsub(Config.store + "/", "")
    all_posts[data['date']]['title'] = data['title']
    all_posts[data['date']]['body']  = body
  end

  all_posts.sort.reverse

end

all_posts je “хаш хашева” (следи објашњење!). Dir.glob чита фајлове из Config.store и попуњава all_posts.

Покренем тест и … он падне:

1) blurgh get_posts should return posts
   Failure/Error: YAML.should_receive(:load_file).with("setup.yaml").and_return({"title" => "Naslov", "store" => "spec/fixtures"})
     (Syck).load_file("setup.yaml")
         expected: 1 time
         received: 3 times

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

Преправим методу у:

def get_posts

  all_posts = Hash.new {
    |h,k| h[k] = Hash.new(&h.default_proc) 
  }

  store = Config.store
  post_dir = File.join(store + "/" + "*.md")

  Dir.glob(post_dir).each do |post|
    header, body = File.readlines(post, "")
    data = YAML.load(header)
    all_posts[data['date']]['url']   = post.gsub("\.md", "").gsub(store + "/", "")
    all_posts[data['date']]['title'] = data['title']
    all_posts[data['date']]['body']  = body
  end

  all_posts.sort.reverse

end

покренем тест и сада прође. Што значи да треба да се убаци у гит:

$ git status
# On branch posts
# 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/fixtures/
no changes added to commit (use "git add" and/or "git commit -a")
$ git add blurgh.rb spec/blurgh_spec.rb spec/fixtures/
$ git status
# On branch posts
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   blurgh.rb
#   modified:   spec/blurgh_spec.rb
#   new file:   spec/fixtures/let.md
#   new file:   spec/fixtures/o-kapadokiji.md
#
$ git commit -m "урађена метода get_posts"
[posts fba900e] урађена метода get_posts
 4 files changed, 81 insertions(+), 31 deletions(-)
 rewrite spec/blurgh_spec.rb (73%)
 create mode 100644 spec/fixtures/let.md
 create mode 100644 spec/fixtures/o-kapadokiji.md

Наставља се… (у следећој епизоди RSpec пије пиво и осваја свет)

Да видим како би то сада могао да организујем да се листа чланака прикаже на почетној страни.

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

context "on the index page" do
  it "the posts URLs should be listed" do
    get '/'
    last_response.body.should match("let")
    last_response.body.should match("o-kapadokiji")
  end
end

под **describe “get_posts” **, покренем тестирање и оно пада:

1) blurgh get_posts on the index page the posts URLs should be listed
   Failure/Error: last_response.body.should match("let")
     expected "<html>\n  <body>\n    <h1>Пази сад</h1>\n\n  </body>\n</html>\n" to match "let"
   # ./spec/blurgh_spec.rb:47:in `block (4 levels) in <top (required)>'

што ми указује да мој тест чита прави setup.yaml, што треба исправити тако што наместим да се YAML објекат учита пре самог теста. То урадим на следећи начин, тако што омогућим у before блоку:

describe "get_posts" do

  before do
    YAML.should_receive(:load_file).with("setup.yaml")\
      .and_return({"title" => "Naslov", "store" => "spec/fixtures"})
  end

  it "should return posts" do
    first_article = File.readlines("spec/fixtures/o-kapadokiji.md", "")[1]
    second_article = File.readlines("spec/fixtures/let.md", "")[1]

    get_posts.should == \
    [[20110325,  {"url" => "let", "title" => "Авионски лет", "body" => "#{second_article}"}],\
     [20110324, {"url" => "o-kapadokiji", "title" => "Кападокија", "body" => "#{first_article}"}]]
  end

  context "on the index page" do
    it "the posts URLs should be listed" do
      get '/'
      last_response.body.should match("let")
      last_response.body.should match("o-kapadokiji")
    end
  end

end

Тест опет пада, али са садржајем који сам ја дефинисао у тесту:

1) blurgh get_posts on the index page the posts URLs should be listed
   Failure/Error: last_response.body.should match("let")
     expected "<html>\n  <body>\n    <h1>Naslov</h1>\n\n  </body>\n</html>\n" to match "let"
   # ./spec/blurgh_spec.rb:53:in `block (4 levels) in <top (required)>'    

Сада да наместим да index.html добије линкове чланака, преправим оригинални get ‘/’ у:

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

и у index.erb додам:

<% @posts.each do |post| %>
   <%= post[1]['url'] %>
<% end %>    

покренем тест, али овај пут само одређени, задњи тест на којем радим:

bundle exec rspec -e "blurgh get_posts on the index page" spec/blurgh_spec.rb         

резултат је:

Failures:

    1) blurgh get_posts on the index page the posts URLs should be listed
       Failure/Error: YAML.should_receive(:load_file).with("setup.yaml")\
         (Syck).load_file("setup.yaml")
             expected: 1 time
             received: 2 times
       # ./spec/blurgh_spec.rb:36:in `block (3 levels) in <top (required)>'

што ми говори да очекивани резултат теста је тачан, али да се конфигурациони фајл чита два пута. Једном се чита:

@title = Config.title

а други пут:

@posts = get_posts

Могао бих да преправим Config модул да чита резултате само једном и да их све врати. Нешто овако:

self.all
  options["title"], options["store"]
end

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

module Config
  # TODO: moze biti efikasnije

Сада да подесим тест:

YAML.should_receive(:load_file).exactly(2).times.with("setup.yaml")\
  .and_return({"title" => "Naslov", "store" => "spec/fixtures"})

Додао сам: .exactly(2).times што ће омогућити да тест прође:

$ bundle exec rspec -e "blurgh get_posts on the index page" spec/blurgh_spec.rb
Run filtered using {:full_description=>/(?-mix:blurgh get_posts on the index page)/}

blurgh
  routes and content
    the view
  get_posts
    on the index page
      the posts URLs should be listed

Finished in 0.02366 seconds
1 example, 0 failures

Покренем тест за цео програм и добијам следећу грешку за четири случаја:

blurgh
  routes and content
    should respond to / (FAILED - 1)
    should have a title (FAILED - 2)
    the view
      should have a body html elements (FAILED - 3)
  get_posts
    should return posts (FAILED - 4)

 ...    

 Failure/Error: YAML.should_receive(:load_file).with("setup.yaml").and_return({"title" => "Naslov", "store" => "posts"})
   (Syck).load_file("setup.yaml")
       expected: 1 time
       received: 2 times    

И то исто могу да исправим тако што учиним исту измену и за те тестове, али опет добијам следећу грешку:

1) blurgh get_posts should return posts
   Failure/Error: YAML.should_receive(:load_file).exactly(2).times.with("setup.yaml")\
     (Syck).load_file("setup.yaml")
         expected: 2 times
         received: 1 time
   # ./spec/blurgh_spec.rb:37:in `block (3 levels) in <top (required)>'

Што ми указује да ћу више труда потрошити на измени тестова, него да применим нову методу all у Config модулу. Зато вратим ове две задње измене и додам нови тест:

describe ".all" do
  it "should return all the values" do
    YAML.should_receive(:load_file).with("setup.yaml")\
      .and_return({"title" => "Naslov", "store" => "posts"}) 
    Config.all.should == {"title" => "Naslov", "store" => "posts"}
  end
end

са методом:

def self.all
  options
end

Покренм тестове за Config:

$ bundle exec rspec spec/config_spec.rb     

сви прођу.

Сада да преправим тестове за blurgh:

it "should return posts" do
  first_article = File.readlines("spec/fixtures/o-kapadokiji.md", "")[1]
  second_article = File.readlines("spec/fixtures/let.md", "")[1]
  store = Config.all['store']

  get_posts(store).should == \
  [[20110325,  {"url" => "let", "title" => "Авионски лет", "body" => "#{second_article}"}],\
   [20110324, {"url" => "o-kapadokiji", "title" => "Кападокија", "body" => "#{first_article}"}]]
end

тест у овом случају неће да прође јер get_posts метода очекује аргумент. Њу преправим у:

def get_posts(store)

  all_posts = Hash.new {
    |h,k| h[k] = Hash.new(&h.default_proc) 
  }

  post_dir = File.join(store + "/" + "*.md")

  Dir.glob(post_dir).each do |post|
    header, body = File.readlines(post, "")
    data = YAML.load(header)
    all_posts[data['date']]['url']   = post.gsub("\.md", "").gsub(store + "/", "")
    all_posts[data['date']]['title'] = data['title']
    all_posts[data['date']]['body']  = body
  end

  all_posts.sort.reverse

end

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

1) blurgh routes and content should respond to /
   Failure/Error: get '/'
   ArgumentError:
     wrong number of arguments (0 for 1)
   ...

Порука је да у get ‘/’ користим нешто са погрешним бројем аргумената. Изменим блок да сада користи аргумент у позиву у get_posts методи:

get '/' do
  blurgh_conf = Config.all
  @title = blurgh_conf['title']
  @posts = get_posts(blurgh_conf['store'])
  erb :index 
end

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

$ bundle exec rake

И пре него што убацим то у гит, било би добро да погледам како то заправо изгледа.

Отворим setup.yaml и додам:

store: spec/fixtures

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

$ shotgun blurgh.rb

И погледам http://localhost:9393/ и видим имена фајлова.

Вратим измену у setup.yaml и додам рад у гит:

$ git status
# On branch posts
# 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
#   modified:   spec/config_spec.rb
#   modified:   views/index.erb
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   .bundle/
no changes added to commit (use "git add" and/or "git commit -a")
$ git add blurgh.rb spec/ views/
$ git status
# On branch posts
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   blurgh.rb
#   modified:   spec/blurgh_spec.rb
#   modified:   spec/config_spec.rb
#   modified:   views/index.erb
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   .bundle/
$ git commit -m "приказује листу чланака на index.html"
[posts 21d9938] приказује листу чланака на index.html
 4 files changed, 45 insertions(+), 13 deletions(-)

Једна напомена овде, кад сам урадио git status приказана су четири фајла. Али ја сам уписао само три аргумента кад сам радио git add:

$ git add blurgh.rb spec/ views/

То је зато што када се гиту дају аргументи са завршном /, он учита фајлове из тог директоријума.

У следећој епизоди: претварање фајлова у линкове и можда још нешто…

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

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

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

$ 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.