Displaying articles with tag rspec

BDD guide #1 When do I mock?

Posted by PunNeng, Sun Mar 09 21:52:00 UTC 2008

ผมเคยสับสนมานานว่าเมื่อไหร่ควรจะ mock และคนที่เคลียร์ปัญหาคาใจผมก็คือเจ้านายผมเอง
เขาบอกโดยสรุปว่า

mock ในส่วนที่เราไม่พิจารณา

เช่นใน controller เราจะพิจารณาที่ logic ของ controller เท่านั้น ในส่วนของ model ที่เราเรียกใช้งานก็ไม่ต้องสนว่าจริงๆ แล้วมันทำงานยังไง เราเลย mock ที่ model ไปเลย ส่วนจะ lock การคืนค่าหรือตัวแปรต่างๆ ก็ stub เอา ส่วน logic ต่างๆ ก็
กระโดนมาส่วน model บ้าง ในส่วนของ model ที่เรา mock ใน controller ไปแล้ว เราควรจะมา test อีกที ทีนี้เราพิจารณา logic ใน model ซึ่งโดยมากแล้วก็ไม่รู้จะ mock อะไร เพราะมันล้วนซึ่งต้องทำงานตาม code ที่เขียนไว้จริงๆ(ดูคล้ายๆ กับ unit test)

มาดูตัวอย่างกัน
ตัวอย่าง 1
อันนี้เป็นตัวอย่างจาก scaffold
สิ่งที่ mock ล้วนซึ่งเป็น model ทั้งนั้น เริ่มจาก before ด้วยการ mock ไปก่อนแล้วตามด้วย stub
ที่ต้อง stub เพราะต้องจำลองผล เพราะเรา mock model มาอีกทีนึง มันจึงไม่แตะ code ของ model จริงๆ
ยังมีส่วนของการ render/redirect ใน controller อีก แต่อันนี้ เราจำเป็นต้องวัดผล(พิจารณา)มันจริงๆ เลยไม่จำเป็นต้อง mock ในส่วนของ render/redirect

ตัวอย่าง 2 จากLuke Redpath

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
describe "A user (in general)" do
  setup do
    @user = User.new
  end

  it "should be invalid without a username" do
    @user.email = 'joe@bloggs.com'
    @user.should_not_be_valid
    @user.errors.on(:username).should_equal "is required" 
    @user.username = 'someusername'
    @user.should_be_valid
  end

  it "should be invalid without an email" do
    @user.username = 'joebloggs'
    @user.should_not_be_valid
    @user.errors.on(:email).should_equal "is required" 
    @user.email = 'joe@bloggs.com'
    @user.should_be_valid
  end
end

ตัวอย่างนี้ไม่มี mock สักตัว เพราะต้องการ test logic ที่มันควรจะเป็นจริงๆ

แต่ถ้า model มีการทำงานร่วมกับ model ตัวอื่น อันนี้ก็น่าจะ mock เป็นต้น

0 comments | Filed Under: Ruby on Rails | Tags: rspec

Mailer Testing

Posted by PunNeng, Wed Feb 20 02:04:00 UTC 2008

Mailer Testing

ไปเจอมาจากที่นี่ ตัด code ที่ผมคิดว่าไม่จำเป็นออก
ในส่วนของ model หน้าตาก็จะได้ประมาณนี้

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
describe EmailSender do
  setup do
    @expected = TMail::Mail.new
    @expected.set_content_type 'text', 'plain', { 'charset' => "utf-8" }
    @expected.mime_version = '1.0'
  end

  it 'should create an email' do
    @expected.subject = 'Rspec'
    @expected.body    = 'Rspec is so cool.'
    @expected.from    = 'myemail@myemail.com'
    @expected.to      = 'youremail@youremail.com'

    EmailSender.create_your_email.encoded.should == @expected.encoded
  end
end

ทดสอบแค่ create พอ ไม่จำเป็นต้องส่ง ถ้าจะส่งก็แค่เรียก EmailSender.deliver(EmailSender.create_your_email)

แต่ปกติแล้วเราาจะใช้ EmailSender.deliveryouremail

ในฝั่ง controller ก็เซ็ตแบบปกติไป

  1
  2
  3
  4
  5
  6
describe YourController do
  it "should send email" do
    EmailSender.should_receive(:deliver_your_email)
    post :your_action
  end
end

พระเอกจริงๆ อยู่ที่ TMail นี่แหละครับ ตัว ActionMailer ก็ใช้ TMail เหมือนกัน

0 comments | Filed Under: Ruby on Rails | Tags: rspec

differences between stub! and should_receive

Posted by PunNeng, Sun Feb 10 16:12:00 UTC 2008

ใน rspec มีศัพท์แสง(method)หลายๆ คำที่ชวนทำผมงง เช่น mock หรือ stub พอเขียนไปเรื่อยๆ ก็มี should_receive โผล่มาอีก ตอน return ก็มี and_return เข้ามาอีก ตอนแรกกับ rspec นี่ ผมแทบร้องไห้ คือพยายามทำความเข้าใจกับมัน แต่ก็ไม่เข้าใจว่า ไอ้ stub นี่ ทำไปทำไม ?? หรือ and_return นี่ มันคือการตรวจสอบค่า return หรือเปล่า ??

หลังจากที่ลองมันไปสักพัก ปรากฏว่ามีอยู่สองอย่างเท่านั้นที่ผมทำยังไงก็ไม่เข้าใจสักที คือ stub! กับ should_receive แต่ตัวที่ทำให้ผมงงจริงๆ กับสองตัวนี้ คงจะเป็น and_return มากกว่า จนแล้วจนรอดก็ไปเจอพระเอกขี่ม้าขาวจนได้ เป็นตัวอย่างในเว็บของ rspec เอง

A.stub!(:msg).and_return(:default_value)
A.should_receive(:msg).with(:arg).and_return(:special_value)
A.msg
=> :default_value
A.msg(:any_other_arg)
=> :default_value
A.msg(:arg)
=> :special_value
A.msg(:any_other_other_arg)
=> :default_value
A.msg
=> :default_value

ความแตกต่าง
stub! คือ การจำลองพฤติกรรมของ method ว่าควรจะรับอะไรแล้วส่งอะไร (คล้ายๆ กับ mock แต่ไม่ใช่ mock)
should_receive คือ การตรวจสอบว่า method หรือ variable ที่เราคาดหวังไว้ เป็นไปตามที่เราคิดไว้หรือเปล่า(แต่จะไม่ตรวจสอบการคืนค่า)
and_return คือ การกำหนดการคืนค่า

A.stub!(:msg).and_return(:default_value)
จำลองไว้ก่อนว่าถ้ารับ :msg มา จะคืนค่าเป็น :default_value
A.should_receive(:msg).with(:arg).and_return(:special_value)
ตรวจสอบว่า มันควรจะได้รับ :msg พร้อมกับ :arg เป็น argument และจะคืนค่าเป็น :special_value
A.msg
=> :default_value
หลังจากที่มันถูก stub ไปแล้ว การเรียก method จะคืนด่ามาเป็น :default_value แต่ถ้าไม่ถูก stub มาก่อน บรรทัดนี้จะกลายเป็น method undefined ทันที
A.msg(:any_other_arg)
=> :default_value
เหมือนกรณีข้างบน แต่คราวนี้รับ :any_other_arg การคืนค่าจะเป็น :default_value เหมือนเดิม แต่เรา stub เพิ่มเป็น
A.should_receive(:msg).with(:any_other_arg).and_return(:any_other_value)
บรรทัดนี้จะคืนค่าเป็น :any_other_value ทันที
A.msg(:arg)
=> :special_value
สำหรับบรรทัดนี้ จะทำให้ผลการคาดหวังเป็นจริงทันที เพราะเราคาดหวังไว้ว่า :msg ควรจะถูกเรียกพร้อมกับ :arg เป็น argument ตรงส่วนนี้แหละ ทำให้ผมงงตอนแรก เพราะดันไปเข้าใจผิดว่าเราคาดหวังการ return เข้ามาด้วย แต่จริงๆ แล้ว and_return เป็นแค่การกำหนดการคืนค่าหลังจาก method ถูกเรียก เท่านั้น
A.msg(:any_other_other_arg)
=> :default_value
A.msg
=> :default_value

สองบรรทัดนี้ก็เหมือนๆ กับกรณีข้างบน

สรุปสิ่งที่ผมเข้าใจผิดก็คือ ผมพยายามจะมองให้มันกลายเป็น unit test ให้ได้ แต่ผมกลับลืมไปว่า BDD จะพิจารณาที่พฤติกรรมเท่านั้น ซึ่งตัวอย่างที่แสดงให้ดู ก็พิจารณาแค่พฤติกรรมของมันจริงๆ
แต่ถ้าจะให้มันเป็น unit test จริงๆ เราสามารถไปเขียน spec สำหรับ test ใน model_spec ครับ ถ้าเราจะพิจารณาไปที่ model หรือ ถ้าเราพิจารณาไปที่ controller ก็เขียนตรวจสอบค่าได้บน controller_spec ได้เลยเหมือนกัน(shoud equal, ==, be หลายตัวเลย) สำหรับตัวอย่าง ไว้จะทยอยเขียนครับ

3 comments | Filed Under: Ruby on Rails | Tags: rspec

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

2 comments | Filed Under: Ruby on Rails | Tags: rspec

Rspec Autotesting with libnotify( on Ubuntu)

Posted by PunNeng, Mon Oct 15 01:01:00 UTC 2007

ก็เคย set บน os x ไปแล้ว คราวนี้จะมา set บน ubuntu กันบ้าง

พระเอกคราวนี้เห็นจะเป็น libnotify

เริ่มต้นเหมือนเดิม ติดตั้ง rspec zentest redgreen ด้วย gem แล้วก็ plugin ของ rails ที่ชื่อ rspec และ rspec_on_rails ไปก่อน แล้วมาติดตั้งเจ้า libnotify

$ sudo apt-get install libnotify-bin

จากนั้นก็สร้าง .autotest เหมือนเดิม ไว้ที่ home

  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
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
module Autotest::GnomeNotify

  # Time notification will be displayed before disappearing automatically
  EXPIRATION_IN_SECONDS = 2
  ERROR_STOCK_ICON = "gtk-dialog-error"
  SUCCESS_STOCK_ICON = "gtk-dialog-info"
  PENDING_STOCK_ICON = "gtk-dialog-warning"

  Autotest.add_hook :ran_command do |at|
    if at.results.empty? # break out gracefully if a test file throws an uncaught exception
      notify ERROR_STOCK_ICON, "Syntax error", ""
    else
      output = format_input( at.results )
      display_notification( output )
    end
  end

  private

  def self.notify stock_icon, title, message
    options = "-t #{EXPIRATION_IN_SECONDS * 1000} -i #{stock_icon}"
    system "notify-send #{options} '#{title}' '#{message}'"
  end

  # isolate the example, failure, pending line like "12 examples, 4 failures, 5 pending"
  # and remove color codes
  def self.format_input( input )
    output = input.detect{ |result| result.include? "example"}
    output = remove_color_codes( output )
  end

  def self.display_notification( output )
    examples, failures, pending = output.split(", ")
    if failures.to_i > 0
      notify ERROR_STOCK_ICON, "Tests Failed", output
    elsif pending.to_i > 0
      notify PENDING_STOCK_ICON, "Tests Pending", output
    else
      notify SUCCESS_STOCK_ICON, "Tests Passed", output
    end
  end


  def self.remove_color_codes( string )
    string.match( /m(.+)\e/)[1]
  end

end

หน้าตาก็จะประมาณนี้

autotest_on_ubuntu

ปล. post อันที่แล้วก็ว่าไปแล้วทีนึง คราวนี้ขอโฆษณาให้คุณวุธ(ไม่รู้สะกดชื่อถูกหรือเปล่า)เต็มๆ ผมไปนั่งร้านกาแฟบ่อยครับ ทุกเสาร์อาทิตย์ แล้วก็ใช้ ubuntu นี่แหละครับ ร้านกาแฟเดียวกันที่ mk เคยเอาลง blog ชื่อร้านว่า Cofe' De Woody อยากคุยเรื่อง rails กับผมก็มาเจอกันได้ :)
ปอ. ผมไม่ทำ rails บน windows นะครับ :) ถ้าอยากใช้ rspec ก็ไปที่ ph7 นะครับ ผมก็เอาตัว libnotify มาจากที่นี่แหละครับ

0 comments | Filed Under: Ruby on Rails | Tags: rspec

codegent: we're hiring