Learning Rspec form Rspec Scaffold

Posted by PunNeng, Sun Nov 25 22:01:00 UTC 2007

ค้างไว้นาน ว่าจะมาเขียนต่อตั้งนานละ(ต่อจากอันนี้)
คราวนี้มีตัวอย่างง่ายๆ จาก rspec_scaffold

ก่อนจะได้ตัวอย่างนี้ คงต้องติดตั้งกันก่อน
คราวก่อนๆ เคยติดตั้ง rspec ไปแล้วจาก

$ sudo gem install rspec

แต่เรายังต้องเตรียมอะไรอีกนิดหน่อยสำหรับทำงานร่วมกับ rails โดยติดตั้งจาก plugins 2 ตัว

$ ruby script/plugin install -x svn://rubyforge.org/var/svn/rspec/trunk/rspec
$ ruby script/plugin install -x svn://rubyforge.org/var/svn/rspec/trunk/rspec_on_rails

เอา -x ออก ถ้าไม่ได้ใช้ svn หรือใช้ svn แต่ไม่ต้องการให้มันติดไปกับงานของเรา

แล้วสั่งสร้าง spec ด้วย

$ ruby script/generate rspec

ถ้าต้องการจะสั่งให้ spec ทำงาน ให้สั่งว่า

$ rake spec

แต่ผมใช้ autotest บน osx หรือ ubuntu ก็เลยสบายหน่อย

จากนั้นสั่งสร้าง rspec ด้วยคำสั่ง

$ ruby script/generate rspec_scaffold account

เนื่องจากผมใช้ Rails 2.0 การ generate ตัวนี้ จะออกมาในแนว restful
หลังจากสั่งแล้ว ผมจะได้ controller ที่ชื่อว่า accounts และ model ที่ชื่อว่า account ในคราวเดียว และรวมถึงชุด rspec ด้วย

ลองไปดูที่ rspec/ ดู จะเจอ spec files อยู่ชุดนึง ประกอบด้วย controllers/ fixtures/ helper/ models/ views/
ลองเข้าไปรื้อค้นดูได้ ให้เข้าไปใน controllers จะเจอ accounts_controller_spec.rb ข้างในจะบรรจุ code อยู่ชุดนึง ผมจะยกตัวอย่างมาสักนิดละกัน

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
describe AccountsController, "handling POST /accounts" do

  before do
    @account = mock_model(Account, :to_param => "1")
    Account.stub!(:new).and_return(@account)
  end

  def post_with_successful_save
    @account.should_receive(:save).and_return(true)
    post :create, :account => {}
  end

  def post_with_failed_save
    @account.should_receive(:save).and_return(false)
    post :create, :account => {}
  end

  it "should create a new account" do
    Account.should_receive(:new).with({}).and_return(@account)
    post_with_successful_save
  end

  it "should redirect to the new account on successful save" do
    post_with_successful_save
    response.should redirect_to(account_url("1"))
  end

  it "should re-render 'new' on failed save" do
    post_with_failed_save
    response.should render_template('new')
  end
end

เราจะศึกษา rspec จาก code ชุดนี้ละกัน ลองมาถอดมันออกมาเป็นพฤติกรรมด้วยภาษาง่ายๆ เป็นการอธิบายก่อนดีกว่า

สร้าง before block

จะถูกเรียกทุกๆ ครั้งที่ spec ถูกเรียก ใน code นี้ จะทำหน้าที่เตรียมข้อมูลต่างๆ ที่เราจะไว้ตรวจสอบ
ซึ่งในนี้ ใส่ id ไปเป็น 1 ในการจำลอง account model แล้ว ก็สั่ง new method สำหรับ Account สำหรับ test แล้วให้มันคืนค่า @account กลับมา(stub!)
โดย stub! คือการสั่งให้ object นั้นๆ เตรียมสิ่งที่เราจะบังคับให้มัน เช่นในตัวอย่าง บังคับว่า Account ถูกเรียกด้วย new method แล้วให้คืนค่าด้วย @account โดยไม่สนว่า code จริงๆ ของ method นั้นๆ มันจะทำอะไร หรือคืนค่าด้วยอะไร
ซึ่งจริงๆ แล้ว ที่มัน new ขึ้นมา ไม่ใช่ Account ที่ใช้ใน business logic ของเรา แต่เป็น Account ที่ใช้สำหรับ test เพราะ @account ที่เราสร้างขึ้นมา ถูก mock(จำลอง) ขึ้นมาอีกทีนึง ซึ่งมันจะไม่ไปแตะตัว model จริงๆ จึงต้องสั่ง new ใน layer ของการ test หรือเรียกง่ายๆ ว่าเป็นการ initial ค่าเริ่มต้นนั่นเอง

มัน(AccountsController) ควรจะสร้าง account ใหม่

สิ่งที่เราคาดหวังจาก spec นี้คือ มันควรจะ new แล้ว save ได้อย่างสมบูรณ์
มาถึงตรงนี้ มันจะทำงานในส่วนของ before block ก่อน Account model ควรจะเรียก new method และควรจะคืนค่าเหมือนๆ กับ @account ที่เราสร้างไว้ตอนแรก (ใน before block)
//จริงๆ แล้วมันควรจะใส่ parameter ด้วย เพราะตอน new object จริงๆ แล้ว ต้องใส่ properties อีกหลายตัว
นี่เป็นการเขียนการคาดการของเรา สิ่งที่เราหวังว่างานของเรา ควรจะทำ(Expectation)
ยังมี expectation อีกก้อนนึงคือ
Account model ควรจะเรียก save method และควรจะคืนค่ามาเป็น true ด้วย
spec ที่ scaffold สร้างมาให้เรา มีแค่นี้ แล้วมันก็จะทำการเรียก method นั้นๆ และส่ง params ไปด้วยอีกตัว เพราะกำหนดตอนแรกไปว่า .with({})
จบ 1 spec

มันควรจะ Redirect ไปที่ account ใหม่ที่สร้างขึ้นมา

สิ่งที่เราคาดหวังจาก spec นี้คือ ถ้า save ได้อย่างสมบูรณ์แล้ว ควรจะ redirect ไปที่หน้า account ใหม่ที่สร้างขึ้นมา
หลังจากที่ before block ทำการ initial ค่าให้แล้ว
ก็มี expectation อีกชุดนึงคือ
Account model ควรจะเรียก save method และควรจะคืนค่ามาเป็น true และก็สั่ง POST เรียกไปยัง method นั้นๆ แต่ยังไม่หมด เพราะสิ่งที่เราจะพิจารณาคือการ render หน้าใหม่
แต่ก่อนจะมาตรวจสอบ render หน้าใหม่ ผมอยากจะชี้ให้เห็นอีกหนึ่งจุดคือ เราไม่จำเป็นต้องตรวจสอบการ new ใหม่เพราะเราตรวจไปแล้วใน spec ข้างบน แต่ถ้าเราใส่ should_receive(:new) เข้าไป ก็จะทำงานได้ตามปกติ
จากนั้นก็มาสั่งต่อว่ามันควรจะ redirect ไปที่หน้า account_url("1") ซึ่ง account_url("1") นี้ จะทำการคืน /account/1 มาให้
จบอีก 1 spec แค่นี้

มันควรจะ render หน้า new อีกที ถ้าเกิดการ save ไม่เสร็จสิ้น

สิ่งที่เราคาดหวังจาก spec นี้คือ save แล้วไม่สมบูรณ์ มันก็ควรจะ render ไปยัง new action
ก็เหมือนๆ เดิม แต่สิ่งที่ต่างออกไปคือ เราคาดหวังว่าถ้า save แล้ว failed มันควรจะ render นะครับ ไม่ใช่ redirect ไปยัง new
expectation เราคือคาดหวังว่ามันจะ render_template

นี่ก็เป็น spec ง่ายๆ ที่เราจะได้จากการใช้ rspec_scaffold สร้าง

มาศึกษาต่อว่า มันทำงานยังไง มีความสัมพันธ์กับ controller ยังไง
กลับเข้าไปใน accounts_controller.rb ไปที่ action create

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
def create
    @account = Account.new(params[:account])

    respond_to do |format|
      if @account.save
        flash[:notice] = 'Account was successfully created.'
        format.html { redirect_to(@account) }
        format.xml  { render :xml => @account, :status => :created, :location => @account }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @account.errors, :status => :unprocessable_entity }
      end
    end
  end

ลองเทียบกับ spec ข้างบนดูนะครับ
code ชุดนี้ จริงๆ เราต้องเขียนหลังจากที่เราเขียน spec ไปแล้ว ให้สัมพันธ์กับ spec ที่เราเขียนไป ซึ่ง code นี้ ก็ทำตามที่เรา(rspec_scaffold) กำหนดไว้ทุกประการ

สรุปหน่อย
เราจะเห็นผลของการทดสอบ spec ก็ต่อเมื่อเราสั่ง rake spec หรือว่าใช้ autotest เหมือนที่ผมใช้
สำหรับการแปล spec เป็นภาษาง่ายๆ ก็น่าจะช่วย guide ให้เราเห็นแนวทางการเขียน spec อย่างน้อยก็ผมคนนึงนี่แหละ
คิดว่าเราจะทำอะไร เขียนไปใน spec ก่อน แล้วค่อยมา implement ให้ตรงกับ spec ที่เรากำหนดไว้

นอกจากนี้ ใน spec file ยังมี spec ที่เหลืออีกอีกหลายตัวครับ ในตัวอย่างที่ผมอธิบายไป เป็นแค่การ POST เพื่อที่จะ create แค่นั้นเอง ยังเหลือการ update show delete อีก
สำหรับเรื่องการ stub หรือ mock เหมือนจะเรื่องเล็ก แต่จริงๆ แล้วเป็นเรื่องที่วุ่นวายเหมือนกัน คงยกยอดไปไว้คราวหน้า(นู้นนนนน)ครับ

ข้อมูลจาก Rspec Home

Filed Under: Ruby on Rails | Tags: behavior driven development rspec ruby on rails

Comments

  1. Patrickz 11.28.07 / 15PM

    แวะมาดู มาเยี่ยม มาทักทาย

  2. PunNeng 11.29.07 / 00AM

    คาราวะพี่ pat หนึ่งจอก :)

Have your say

A name is required. You may use HTML in your comments.




codegent: we're hiring