Ruby on Rails tikrai daro įspūdį pradedantiesiems kaip greit ir natūraliai galima sukurti web aplikaciją. Tačiau panorėjus pasekti Rails guru pėdomis, kurie kalbant apie kokybę visi vienu balsu tvirtina “always, always test!”, tikrai galima atsimušti į informacijos pradedantiesiems trūkumą, ypač jeigu norit pasikinkyti ne standartinį Test::Unit. Ne pirmą kartą girdint tokį nusiskundimą, nusprendžiau sudėlioti trumpą RSpec ir machinist pradžiamokslį – kaip instaliuoti, kaip susikonfigūruoti, nuo ko ir kaip pradėti, kur/kaip ieškoti informacijos.

Kodėl būtent RSpec ir machinist?

RSpec leidžia aprašyti sistemos elgseną labiau “ruby way”, negu Test::Unit, nors pastarasis vis dar dažnai naudojamas. RSpec laikomas behaviour-driven development įrankiu, nors šiuo atveju parodysiu tiesiog test-driven development panaudojimą.

Sistemai augant išlaikyti reikiamą fixtures kiekį ir struktūra yra tikras vargas, todėl apžiūrėkime machinist, kuris suteikia galimybę sugeneruoti duomenis, kurių reikšmės mums nesvarbios. Kodėl ne factory_girl? Tiesiog, kodas atrodo gražiau ;-)

Sąlygos

Šis pradžiamokslis nėra skirtas įrodyti, kad jums reikia testuoti. Jis taip pat nebus naudingas pažengusiems, kuriems ši informacija gali atrodyti savaime suprantama. Visgi turite būti susipažinę su pagrindiniais Ruby on Rails principais.

Pavyzdžiai skirti Rails 2.3.4, RSpec 1.2.9, machinist 1.0.3 ir faker 0.3.1, tačiau greičiausiai veiks ir su kitomis šių paketų versijomis.

Pradžia

Tarkime, kad norime padaryti aukcioną, kuriame galima kelti kainą iki tam tikros datos.

rails auction_example

config/environments/test.rb pridedame:

config.gem "rspec", :lib => false, :version => ">= 1.2.9"
config.gem "rspec-rails", :lib => false, :version => ">= 1.2.9"

Instaliuojame, jeigu dar neturime:

rake gems:install RAILS_ENV=test

Ir sugeneruojame RSpec failiukus:

ruby script/generate rspec

Jau pasiruošę pradėti!

Pirma pavara

Akivaizdu, kad turėsime prekes, kurias norėsime brangiai prakalti piniguotiems dėdėms iš užsienio:

ruby script/generate rspec_scaffold product name:string auction_ends_at:datetime

rspec_scaffold generatorius parūpino mums ne tik įprastinius scaffold failus, bet ir pradinius griaučius testavimui. Analogiškos komandos yra rspec_controller, rspec_model.

Faile spec/models/product_spec.rb rasite jau tokį tekstuką:

require 'spec_helper'
 
describe Product do
  before(:each) do
    @valid_attributes = {
      :name => "value for name",
      :auction_ends_at => Time.now
    }
  end
 
  it "should create a new instance given valid attributes" do
    Product.create!(@valid_attributes)
  end
end

Manau, kad nieko papildomai aiškinti nereikia, geriau pulkime ir pažiūrėkime ar tikrai veikia:

rake db:migrate
rake spec
 
#=>
............................. 
Finished in 1.296021 seconds
29 examples, 0 failures

Kol kas viskas kaip per sviestą. Kadangi darome aukcioną, akivaizdu, jog jeigu prekei skirtas laikas baigėsi, daugiau statymų daryti nebegalima. Žinoma, tam prireiks Product modelyje metodo auction_ended?. Pagal TDD pirma turime parašyti testą:

# iškart po it "..." do - end bloko
it "should mark auction as ended if it's so" do
   product = Product.create(@valid_attributes.merge(:auction_ends_at => 2.hours.ago))
   product.auction_ended?.should be_true
end

Ir žinoma, išbandę rake spec gausime pranešimą:

1)
NoMethodError in 'Product should mark auction ended if it's so'
undefined method `auction_ended?' for #Product:0xb731808c
./spec/models/product_spec.rb:17:
 
Finished in 1.070546 seconds
 
30 examples, 1 failure

Dabar jau galima bandyti rašyti kodą, kuris tenkintų esamus testus:

class Product < ActiveRecord::Base
  def auction_ended?
    true
  end
end

Ir jau su tokiu kodu gauname išganingajį pranešimą:

30 examples, 0 failures

Bet juk kodas tai neteisingas, tiesa? Čia yra svarbiausia žinutė - testuokite įvairius variantus. Jeigu metode yra if sąlyga, apeikite visas šakas:

it "shouldn't mark auction as ended if it ends in the future" do
  product = Product.create(@valid_attributes.merge(:auction_ends_at => 2.hours.from_now))
  product.auction_ended?.should be_false
end

Dabar jau, žinoma, turime 1 failure. Teliko pamodifikuoti Product metodą, kad jis atitiktų realybę:

def auction_ended?
  auction_ends_at < Time.now
end

Dabar mūsų parašyti testai ne tik tuščiai prasisuka bet ir praneš apie galimą problemą jeigu ką nors tvarkydami subjaurosim šio metodo prasmę. Realiai toks testavimo tikslas ir yra - būti užtikrintam, kad keičiant ką nors viename gale, kitame viskas veikia tiksliai taip, kaip turėtų.


Antroje dalyje - kam reikalingas machinist, kodėl jis geresnis negu standartiniai fixtures ir pavyzdys kaip testuoti controllerius.

Kad lengviau galėtumėt pabandyti kaip viskas atrodo, sukūriau šios mini-serijos aplikacijos repozitoriją GitHub.