Ruby Language

TDD Rspec ve Ruby on Rails

Test Driven Development (TDD), diğer adlarıyla Test First Development, Test Driven Design olarak bilinmetedir. İlk olarak Kent Beck tarafından ortaya atılmıştır. Kent Beck sadece TDD’nin değil Çevik Programcılık yöntemininde kurucularındandır. Çevik Programcılık ile TDD ayrılmaz iki unsur olarak düşünülebilir. Süreç çok basittir. Test kodlarını yaz. Testleri çalıştır ve hepsi hata verir Programın kodlarını yaz ve […]

Test Driven Development (TDD), diğer adlarıyla Test First Development, Test Driven Design olarak bilinmetedir. İlk olarak Kent Beck tarafından ortaya atılmıştır. Kent Beck sadece TDD’nin değil Çevik Programcılık yöntemininde kurucularındandır. Çevik Programcılık ile TDD ayrılmaz iki unsur olarak düşünülebilir. Süreç çok basittir.

  1. Test kodlarını yaz.
  2. Testleri çalıştır ve hepsi hata verir
  3. Programın kodlarını yaz ve testler başarılı olmasını sağlarız
  4. Başa döneriz

Burada ki mantık yaptığınız her değişikliğin sistemin bütününe nasıl etkilediğini tek tek test etmek yerine bunları yapan kodları yazmamızdır. Bu yazımızda ben hem okunurluğu yüksek olduğunu ve scaffolding mekanizması kolay olduğu için Ruby dilinde, Ruby on Rails ile çok basit bir TDD süreçlerini kullanarak bir yazı yazacağım. Tabii ki konunun çok büyük olması ve test çeşitlerinin çok olmasından dolayı bu yazıda sadece model sınıflarını test edeceğiz.

İlk önce basit bir Personel projesi yapalım.

rails new Personel -T

Burada ki -T ifadesi ROR varsayılan test çatışı için UnitTest kullanmaktadır. Biz bunun yerine Rspec kullanacağımız için -T parametresi ile Unit Test yüklü olmayan bir ROR uygulaması kurduk. Ruby dilinde ki diğer test çatıları hakkında bilgi almak için https://www.ruby-toolbox.com/categories/testing_frameworks linkini ziyaret edebilirsiniz.

cd Personel && rails s

rails s ile webrick sunucumuzu çalıştırabiliriz. Bu sayede http://localhost:3000 adresi ile uygulamamızın ana sayfasını görebiliriz.

Şimdi uygulamamıza rspec test çatışını kuralım. İlk yapacağımız Gemfile dosyamıza test için kullanacağımız rspec ve factory_girl_rails gemlerini eklemek olacakdır.

group :test do
  gem 'factory_girl_rails'
end

gem "rspec-rails", :group => [:test, :development]

Yukarıda bulunan bloğu Gemfile’ın en son satırına ekliyoruz. Burada ki factory_girl_rails gemi bizim testlerimiz için kukla test nesleleri oluşturmak içindir. Bundan sonraki adımımız Bundle gemi ile gemleri düzenlemektir.

bundle install

Bundle gemi, Ruby dilinde ki Gemlerin birbirleri arasındaki bağımlılıklara bakarak ihtiyaç duyulan diğer gemleri yüklemeye yarar. Bu süreci bizim için yönetir. Kullanacağımız factory_girl_rails ve rspec-rails gemleri ve bunların ihtiyaç bulunduğu diğer gemler bundle gemi sayesinde sisteme yüklendikten sonra artık rspec’i uygulamamıza kurabiliriz.

rails g rspec:install

Komutu ile rspec’i uygulamamıza kurarız. Bu komut aşağıdaki dosya ve dizinleri yaratır.

      create  .rspec
      create  spec
      create  spec/spec_helper.rb

Artık rspec ile rails entegre bir şekilde çalışamya başladı. Bunu söyle anlatayım rails kod yaratacılarını kullandığınız sürece rspec kodlarıda otomatik sizin için yaratılacak. Hemen Personel projemiz için bir Person modeli oluşturalım. Testlerimiz için bu person modelinin ismi ve tekil bir ismi olsun.

rails g scaffold Person name:string email:string

Aşağıda ki çıktıa gördüğünüz gibi rails bizim için gerekli bütün kodları oluşturdu ve oluşturduğu kodların içinde rspec kodları da bulunmakta.

      invoke  active_record
      create    db/migrate/20111228133609_create_people.rb
      create    app/models/person.rb
      invoke    rspec
      create      spec/models/person_spec.rb
       route  resources :people
      invoke  scaffold_controller
      create    app/controllers/people_controller.rb
      invoke    erb
      create      app/views/people
      create      app/views/people/index.html.erb
      create      app/views/people/edit.html.erb
      create      app/views/people/show.html.erb
      create      app/views/people/new.html.erb
      create      app/views/people/_form.html.erb
      invoke    rspec
      create      spec/controllers/people_controller_spec.rb
      create      spec/views/people/edit.html.erb_spec.rb
      create      spec/views/people/index.html.erb_spec.rb
      create      spec/views/people/new.html.erb_spec.rb
      create      spec/views/people/show.html.erb_spec.rb
      invoke      helper
      create        spec/helpers/people_helper_spec.rb
      create      spec/routing/people_routing_spec.rb
      invoke      rspec
      create        spec/requests/people_spec.rb
      invoke    helper
      create      app/helpers/people_helper.rb
      invoke      rspec
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/people.js.coffee
      invoke    scss
      create      app/assets/stylesheets/people.css.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.css.scss

Veritabanını yaratmak ve person tablosunu oluşturmak için aşağıdaki komutları çalıştırıyoruz.

rake db:create && rake db:migrate

Rails bizim için view, model ve controller katmanlarında ki kodları oluşturdu. http://localhost:3000 adresinden yeni bir kişi ekleyebilirsiniz. Burada email ve name alanları için bir validasyon henüz yok. Bizim kod yazmaya başlayacağımız kısımda buradan sonra başlamaktadır. Tabii validasyonlardan önce testlerimizi yazacağız.Testlerimizi /spec/models/person_spec.rb dosyasına yazacağız. Burada toplam dört test yazacağız.

  1. Email adresi tekil olmalı
  2. Email adresi boş olmamalı
  3. Email adresi email standartlarında olmalı
  4. Kişinin ismi boş olmamalı

Testlerimizi yazmadan önce karışıklık olmaması için şimdiden testlerimizi çalıştıralım.

rake spec

Karşımıza aşağıdakine benzer bir tablo çıkacaktır.

/home/ooo/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S rspec ./spec/models/person_spec.rb ./spec/helpers/people_helper_spec.rb ./spec/requests/people_spec.rb ./spec/routing/people_routing_spec.rb ./spec/controllers/people_controller_spec.rb ./spec/views/people/show.html.erb_spec.rb ./spec/views/people/new.html.erb_spec.rb ./spec/views/people/index.html.erb_spec.rb ./spec/views/people/edit.html.erb_spec.rb
**............................

Pending:
  Person add some examples to (or delete) /var/www/test/Personel/spec/models/person_spec.rb
    # Not Yet Implemented
    # ./spec/models/person_spec.rb:4
  PeopleHelper add some examples to (or delete) /var/www/test/Personel/spec/helpers/people_helper_spec.rb
    # Not Yet Implemented
    # ./spec/helpers/people_helper_spec.rb:14

Finished in 5.89 seconds
30 examples, 0 failures, 2 pending

Çalıştırılan test dosyalarının isimleri ve daha önemlisi 5.89 sn testlerin sürdüğünü, 30 tane test olduğu bunların 0 tanesinin hata verdiği ve 2 tanesinin henüz yazılmadığına dair bir çıktı alacağız. Burada altını çizmek istediğim iki konu var, birincisi testlerin süresi gözünüzü korkutmasın benim bilgisayarım 2005 yılında üretilen bir bilgisayardır. İkinci konu bu 30 adet testi scaffolding bizim yerimize yaratmıştır. Ne nereden geldi diye şaşırmayın. Bu bilgileri verdikten sonra artık testlerimizi yazabiliriz.

İlk önce testlerimiz düzenli olması için isim ve email için yapılacak testleri gruplayacağım. Bunun için describe methodunu kullanacağım.

require 'spec_helper'

describe Person do
  describe "email" do

  end

  describe "name" do

  end
end

İkinci olarak testlerimde kullanılmak üzere kukla Person nesneleri yaratmak için factory_girl gemini kullacağız. Bunun için /spec/factories.rb isminde bir dosya oluşturacağız.

Factory.define :person do |f|
  f.name 'Isim Soyisim'
  f.sequence(:email) { |n| "[email protected]" }
end

Hemen burada bir not vereyeyim, bütün kulka neslelerimizin ismini “Isım Soyisim” yaptık ancak mail adreslerini f.sequence(:email) methodu ile [email protected], [email protected] gibi birer artırdık çünkü mailler tekil olmak zorundadır.

Artık person_spec.rb dosyamıza bütün testlerden önce çalışmak üzere iki adet instance tanımlıyoruz.

require 'spec_helper'
describe Person do
  before(:each) do
    @person = Factory(:person)
    @new_person = Person.new
  end

  #kodun devamı
end

Bu işlemi before methodunda yapıyoruz. Bütün yazacağımız kodlarda @person ve @new_person’ı kullanabileceğiz. @person veritabanına yazılmış gibi davranırken, @new_person instance alınmış ama henüz kaydet methodu çalıştırılmamış Personları temsil ediyor.

Email için olan üç adet testimizi yazalım.

  describe "email" do
    it "should be presence" do
      @new_person.should be_invalid
      @new_person.errors[:email].should include("can't be blank")
    end

    it "should be uniqueness" do
      @new_person.email = @person.email
      @new_person.should be_invalid
      @new_person.errors[:email].should include("has already been taken")
    end

    it "should be email" do
      @new_person.email = 'invalid email'
      @new_person.should be_invalid
      @new_person.errors[:email].should include("is invalid")
    end
  end

Şimdi tek tek testleri açıklayalım.

it “should be presence” do

Bu testte email alanının zorunlu bir alan olduğunu test ediyoruz.

@new_person.should be_invalid
@new_person.errors[:email].should include("can't be blank")

@new_person.should be_invalid satırı @new_person instancenın veritabanına kaydedilemeyeceğini söylüyor. Yani instance validasyondan geçemez. Invalid methodu ile hangi parametrelerde hata olduğu @new_person.errors değişkenine atanıyor.

@new_person.errors[:email].should include(“can’t be blank”) satırında ise invalid işleminden sonra @new_person instancenın errors[:email] parametresinin “can’t be blank” stringine eşit olduğunu söylüyoruz.

it “should be uniqueness” do

Bu teste email alanının tekil olduğunu kontrol ediyoruz.

@new_person.email = @person.email
@new_person.should be_invalid
@new_person.errors[:email].should include("has already been taken")

@new_person.email = @person.email satırında @new_person’ın email parametresine daha önce oluşturulan ve veritabanına kaydedildiği varsayılan @person’ın email parametresini atıyoruz. Yani bir nevi veritabanında olan bir email atanıyor.

@new_person.should be_invalid satırında @new_person instance’ı validasyondan geçiyormu kontrol ediyoruz. Geçmiyorsa errors parametresine değerleri atıyoruz.

@new_person.errors[:email].should include(“has already been taken”) satırında ise @new_person’ın errors[:email] hash tipindeki parametresinin “has already been taken” stringini içerip içermediğini kontrol ediyoruz.

it “should be email” do

Bu testte emailin belli bir regex formatında olup olmadığını test edeceğiz.

@new_person.email = 'invalid email'
@new_person.should be_invalid
@new_person.errors[:email].should include("is invalid")

@new_person.email = ‘invalid email’ satırında email olarak “invalid email’ stringi atanıyor.
@new_person.should be_invalid satırında @new_person’ın validasyondan geçip geçmediğine bakılıyor ve error parametresi hatalar ile dolduruluyor.
@new_person.errors[:email].should include(“is invalid”) satırında ise emailin modelde bahsedilen regex validasyonuna uymadığı için errors[:email] hashinin “is invalid” stringini içerdiğini kontrol ediyoruz.

Şimdi testlerimizi tekrar çalıştırıyoruz.

rake spec

Çıktımı aşağıdaki gibi oluyor. 32 örnek test, 3 başarısız, 1 bekleyen şekline geliyor. Yukarıda yazdığımız üç adet test gördüğünüz gibi başarısız durumda.

/home/ooo/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S rspec ./spec/models/person_spec.rb ./spec/helpers/people_helper_spec.rb ./spec/requests/people_spec.rb ./spec/routing/people_routing_spec.rb ./spec/controllers/people_controller_spec.rb ./spec/views/people/show.html.erb_spec.rb ./spec/views/people/new.html.erb_spec.rb ./spec/views/people/index.html.erb_spec.rb ./spec/views/people/edit.html.erb_spec.rb
FFF*............................

Pending:
  PeopleHelper add some examples to (or delete) /var/www/test/Personel/spec/helpers/people_helper_spec.rb
    # Not Yet Implemented
    # ./spec/helpers/people_helper_spec.rb:14

Failures:

  1) Person email should be presence
     Failure/Error: @new_person.should be_invalid
       expected invalid? to return true, got false
     # ./spec/models/person_spec.rb:12:in `block (3 levels) in <top (required)>'

  2) Person email should be uniqueness
     Failure/Error: @new_person.should be_invalid
       expected invalid? to return true, got false
     # ./spec/models/person_spec.rb:18:in `block (3 levels) in <top (required)>'

  3) Person email should be email
     Failure/Error: @new_person.should be_invalid
       expected invalid? to return true, got false
     # ./spec/models/person_spec.rb:24:in `block (3 levels) in <top (required)>'

Finished in 5.32 seconds
32 examples, 3 failures, 1 pending

Failed examples:

rspec ./spec/models/person_spec.rb:11 # Person email should be presence
rspec ./spec/models/person_spec.rb:16 # Person email should be uniqueness
rspec ./spec/models/person_spec.rb:22 # Person email should be email
rake aborted!
ruby /home/ooo/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S rspec ./spec/models/person_spec.rb ./spec/helpers/people_helper_spec.rb ./spec/requests/people_spec.rb ./spec/routing/people_routing_spec.rb ./spec/controllers/people_controller_spec.rb ./spec/views/people/show.html.erb_spec.rb ./spec/views/people/new.html.erb_spec.rb ./spec/views/people/index.html.erb_spec.rb ./spec/views/people/edit.html.erb_spec.rb failed

Tasks: TOP => spec
(See full trace by running task with --trace)

Artık amacımız bu testleri başarılı bir hale getirmek. Bunun için /app/models/person.rb dosyasına Person için persence, uniqueness ve regex validasyon yazacağız. Merak etmeyin üç validasyon Rails’da üç satırı geçmiyor. Sizin anlayacağınız makalenin sonuna geldik.

class Person < ActiveRecord::Base
  EMAIL_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i
  validates :email,     :presence => {:on => :create},
                        :uniqueness => true,
                        :format => {:with => EMAIL_REGEX}
end

Yukarıda ki model bu makalenin konusu olmadığı için anlatmıyorum.

Yazdığımız validasyonlar rspec’in people_controller.rb için oluşturduğu /spec/controllers/people_controller_spec.rb dosyasınıda etkilemektedir. İlgili dosyanın 26’ı satırını aşağıdaki gibi güncellersek controller testlerinin kullandığı pearametlerinde doğru oldu olmasını sağlarız.

  def valid_attributes
    {
      :name => "Test user",
      :email=> "[email protected]"
    }
  end

Artık testlerimizi tekrar çalıştırıyoruz.

rake spec

Çıktımız bize 32 testin hepsinin geçtiğini 1 tanesinin henüz yazılmadığını söylüyor.

/home/ooo/.rvm/rubies/ruby-1.9.3-p0/bin/ruby -S rspec ./spec/models/person_spec.rb ./spec/helpers/people_helper_spec.rb ./spec/requests/people_spec.rb ./spec/routing/people_routing_spec.rb ./spec/controllers/people_controller_spec.rb ./spec/views/people/show.html.erb_spec.rb ./spec/views/people/new.html.erb_spec.rb ./spec/views/people/index.html.erb_spec.rb ./spec/views/people/edit.html.erb_spec.rb
...*............................

Pending:
  PeopleHelper add some examples to (or delete) /var/www/test/Personel/spec/helpers/people_helper_spec.rb
    # Not Yet Implemented
    # ./spec/helpers/people_helper_spec.rb:14

Finished in 5.64 seconds
32 examples, 0 failures, 1 pending

Peki bu testleri yazmak gerekli mi? Normalde bir çok yazılımcı ürünleri bittikten sonra bu tür testleri tek tek tarayıcılardan kayıt, güncelleme ve silme işlemlerini yaparak sonrada veritabanına bakarak manuel yapıyorlar. Bu testi yazmak örneğin benim 30 dakika mı aldı diyelim. Bu proje için ben yaptığım her güncellemede bütün sistemi tarayıcıdan ve veritabanından test etmem inanılmaz vakit alacaktır. Özellikle projenin büyümesi, farklı insanların bu proje üzerinde çalışması, gibi etkenler hata yapma olasılığını artıracaktır. Bu tarz testler hem takım halinde çalışmayı sağlarken, hem de orta vadede teste ayrılan zamanı inanılmaz kısaltmaktadır.

Eğer TDD hakkında biraz bilgi sahibi olduysanız ne mutlu bana! Bundan sonra ki süreçlerinizde TDD kullanırsanız ne mutlu size. Her türlü soru ve eleştirinizi esirgemeyin.