Блог у синатри, епизода седма
Било би пожељно да овај систем може да учита чланке и да их прикаже. Идеја је да чита чланке из одређеног директоријума који је наведен у конфигурационом фајлу 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/
То је зато што када се гиту дају аргументи са завршном /, он учита фајлове из тог директоријума.
У следећој епизоди: претварање фајлова у линкове и можда још нешто…