Archive for มีนาคม 2009
แก้ unit test failure ใน RailsSpace หลังอัพเกรดเป็น Rails 2.3
โพสก่อนหน้านี้จบด้วยการเจ๊งของ unit test และก็ไปอาบน้ำนอน
วันนี้เลยลองหาต้นเหตุดูว่าปัญหาเกิดจากอะไร ได้ความว่าต้นเหตุของปัญหาเกิดจาก 2 method ใน user_controller_test เท่านั้นเอง คือ cookie_expires กับ cookie_value
ปัญหาแรกคือ
1) Error:
test_login_success_with_remember_me(UserControllerTest):
NoMethodError: undefined method `expires' for "1":String
/test/functional/user_controller_test.rb:268:in `cookie_expires'
/test/functional/user_controller_test.rb:145:in
`test_login_success_with_remember_me'
ซึ่งเกิดเพราะว่าใน Rails 2.3 นั้นการเรียกใช้ cookies[symbol.to_s] จะไม่คืน cookie object แล้วแต่ว่าคืนค่าของคุ๊กกี้นั้นๆเลย (ลองดู API) ทำให้เราไม่สามารถใช้ expires ได้แล้ว
ที่ทำให้ซับซ้อนยิ่งกว่านั้น (และยังหาทางลัดใน Rails ไม่เจอ) คือเราเข้าถึงคุ๊กกี้ผ่าน @response.headers["Set-Cookie"] ได้เท่านั้น และค่าที่คืนมาจาก header นี้เป็น array ของ string ที่แต่ละ element แทนคุ๊กกี้หนึ่งอัน เช่น
(rdb:1) @response.headers["Set-Cookie"]
["remember_me=1; path=/; expires=Wed, 19-Mar-2014 16:29:09 GMT", "authorization_token=56d1799d501a6ea5f0e612d22ddba211dc4066ae; path=/; expires=Wed, 19-Mar-2014 16:29:09 GMT"]
ในกรณีนี้สิ่งที่เราต้องการ (expires) อยู่ที่ element แรกของ array ซึ่งเราสามารถแก้โค้ดเป็นแบบนี้ได้
def cookie_expires(symbol)
Time.zone.parse(@response.headers["Set-Cookie"][0].split("\;")[2].split("=")[1])
end
ไม่ได้เขียนให้อ่านง่ายมากนัก และก็ไม่ค่อยยืดหยุ่นด้วย แต่ดูเหมือนจะใช้งานได้ สิ่งที่เราทำก็คือเอา item แรกของ array ซึ่งตรงกับคุ๊กกี้ที่เราต้องการ (remember_me) มาแบ่ง string ด้วยเครื่องหมาย semicolon ก่อน แล้วก็เอา item สุดท้ายจากที่แบ่งได้ (expires) มาแบ่งด้วยเครื่องหมายเท่ากับอีกทีเพื่อเอาค่าของ expires มาแปลงเป็นเวลาเพื่อเปรียบเทียบ
แน่นอนว่าอ๊อบเจ็คต์ที่เราคืนกลับไปอาจไม่ใช่เวลาที่ตรงกับเวลาของคุ๊กกี้ที่เราเซ็ตค่าไว้ผ่านคอนโทรลเลอร์ แต่ว่าก็ยังอยู่ใน range การเปรียบเทียบของ test ได้ ซึ่งเพียงพอจะทำให้ผ่านการทดสอบ
อีกปัญหานึงที่เกิดกับ cookie_value ก็ด้วยเหตุผลเดียวกันกับที่เขียนไว้ด้านบน คือการ access hash คุ๊กกี้นั้นจะคืนค่าของคุ๊กกี้เลย ไม่ได้คืน array อีกต่อไป ทำให้เราต้องแก้เป็น
def cookie_value(symbol)
cookies[symbol.to_s]
end
หลังจากแก้ไขเรียบร้อยก็ test อีกทีเพื่อความแน่ใจ
ปล. เดาว่าการที่เราต้องเข้าถึงคุ๊กกี้ผ่าน “Set-Cookie” น่าจะเป็นผลจากการที่ Rails 2.3 เปลี่ยนมาสนับสนุน Rack เต็มรูปแบบแล้วในแบบของ middleware จึงทำให้โครงสร้างของเฟรมเวิร์คเปลี่ยนไปเก็บคุ๊กกี้ที่ HTTP request/response แทน
อัพเกรด RailsSpace เป็น Rails 2.3
เนื่องจาก Rails 2.3 เพิ่งออกเมื่อสองวันที่แล้ว เลยอัพเกรด RailsSpace ไปใช้เวอร์ชั่นใหม่ซะเลย วิธีอัพเกรดก็ง่ายๆ
> sudo gem install rails
> cd rails_space
> rake rails:update
หรือจะลองดูในวิกิก็ได้ จากนั้นก็ทำการแก้ไขเวอร์ชั่นใน environment.rb ให้เป็น
RAILS_GEM_VERSION = '2.3.2' unless defined? RAILS_GEM_VERSION
แล้วก็ลองเล่นๆดู ในหน้าเว็บก็ดูปกติดี แต่ว่ามีเตือนเรื่องโค้ดปลดระวางนิดหน่อย คือ
DEPRECATION WARNING: protect_from_forgery only takes : only and :except options now. :digest and :secret have no effect. (called from ../rails_space/app/controllers/application_controller.rb:12)
วิธีแก้ก็คือ comment โค้ดบรรทัด protect_from_forgery ออกจาก application_controller.rb แล้วคำเตือนก็จะหายไป
ขั้นตอนต่อไปคือลองรันเคสทดสอบ เพื่อดูว่า unit test เรายังอยู่ดีหรือเปล่า
> rake
ปรากฏว่าเจ๊งบางอันว่า
./test/test_helper.rb:22: undefined method `use_transactional_fixtures=' for Test::Unit::TestCase:Class (NoMethodError)
นั่นคงเป็นเพราะว่า Rails เปลี่ยนมาใช้ ActiveSupport::TestCase แทน Test::Unit แล้ว เราจึงต้องแก้ไข test_helper.rb จาก
class Test::Unit::TestCase
เป็น
class ActiveSupport::TestCase
จากนั้นก็ลองรันเทสดูอีกครั้ง ซึ่งคราวนี้สามารถรันได้จนจบกระบวนความ แต่ว่ามี error ใน functional test สามอันคือ
1) Error:
test_login_success_with_remember_me(UserControllerTest):
NoMethodError: undefined method `expires' for "1":String
/test/functional/user_controller_test.rb:268:in `cookie_expires'
/test/functional/user_controller_test.rb:145:in
`test_login_success_with_remember_me'
2) Error:
test_logout_clears_session_and_cookie(UserControllerTest):
NoMethodError: You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.first
/test/functional/user_controller_test.rb:272:in `cookie_value'
/test/functional/user_controller_test.rb:191:in
`test_logout_clears_session_and_cookie'
3) Error:
test_test_a_valid_login(UserControllerTest):
NoMethodError: You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.first
/test/functional/user_controller_test.rb:272:in `cookie_value'
/test/functional/user_controller_test.rb:125:in `test_test_a_valid_login'
อา ดึกแล้ว ไว้ค่อยลองแก้วันหลังละกัน
SyntaxError in Community#index ใน RailsSpace
ตามโค้ดใน Listing 11.1.1 ของ RailsSpace (หน้า 328) เขียนโค้ดแบบนี้
<% form_tag({ :action => "search" }, :method => "get") do %>
...
<% end %>
แต่ว่าถ้าเราเขียนตามตัวอย่างใน Rails 2.2 เราจะเจอ error ว่า
SyntaxError in Community#index
Showing app/views/community/_search_form.html.erb where line #1 raised:
compile error
/Users/Tik/temp/rails_space/app/views/community/_search_form.html.erb:1: syntax error, unexpected tASSOC, expecting '}'
...e=true ; form_tag {:action => "search"}, :method => "get" d...
^
/Users/Tik/temp/rails_space/app/views/community/_search_form.html.erb:1: syntax error, unexpected ',', expecting kEND
...orm_tag {:action => "search"}, :method => "get" do ; @output...
วิธีแก้ก็ง่ายๆ แค่เอา :action ออกจาก hash ก็เรียบร้อย เพราะดูเหมือนว่า Rails 2.2 ไม่ต้องการ hash สำหรับ :action แล้ว
Result Summary สำหรับ will_paginate ใน RailsSpace
ต่อจากโพสต์ที่แล้ว ใน Listing 10.19 มีใช้ item_count, current_page, first_item, และก็ last_item ซึ่งเราไม่มีใน Rails 2.2
การเปลี่ยนโค้ดให้ทำงานกับ will_paginate ก็ไม่ยากนัก เพราะว่า array ที่คืนมาโดย will_paginate ก็มี method พิเศษต่างๆเช่นกันคือ total_pages, total_entries, current_page, และก็ offset เพื่อแทนค่าจำนวนหน้าทั้งหมด, จำนวน item ทั้งหมดในผลลัพธ์, เลขที่หน้าปัจจุบัน, และก็ index ของ item แรกของหน้า (อย่าลืมว่า index ของ array เริ่มต้นที่เลขศูนย์ เพราะฉะนั้นเราต้องบวก 1 ตอนแสดงผล)
โค้ดที่แทนก็กลายเป็น:
Found <%= pluralize(@specs.total_entries, "match") %>.
<% if @specs.total_pages > 1 %>
<% first_item = @specs.offset + 1 %>
<% last_item = @specs.offset + @specs.length %>
Displaying users <%= first_item %>–<%= last_item %>
<% end %>
เท่านี้ก็เรียบร้อย (เนื่องจากว่า will_paginate ไม่มี first_item และ last_item เราจึงต้องคำนวนเอง)
อัพเดท: เพิ่งเปิดเจอลิงก์ของ RailsSpace เมื่อปีที่แล้วที่อธิบายการแปลงเป็น will_paginate คล้ายๆกัันแต่เจ๋งกว่าตรงที่โค้ดของลิงก์นี้ย้าย first_item, last_item, และ paginated ไปไว้ใน WillPaginate class เลย เจ๋ง…ลืมไปว่ากำลังทำงานกับ Ruby อยู่ :p
RailsSpace Pagination ใน Rails 2.2
ใน Listing 10.16 ของ RailsSpace เริ่มแนะนำการใช้งาน pagination หรือการแบ่งหน้าของผลลัพธ์ที่มาจากการ find หา record ในฐานข้อมูล แต่ว่าโค้ดดังกล่าวใช้ไม่ได้ใน Rails 2.2 และเราจะเจอ error แบบนี้:
NoMethodError in CommunityController#index
undefined method `paginate' for #<CommunityController:0x2532e7c>
ปัญหานี้เกิดขึ้นจากการที่ paginate ถูกย้ายออกไปเป็นปลั๊กอินแล้วตั้งแต่ Rails 2.0 ซึ่งหมายความว่าเราไม่มี method นี้ให้ใช้ใน 2.2
เลยลองหาดูว่ามีตัวเลือกอะไรนอกจาก classic_pagination ที่ระบุไว้ในเอกสารอ้างอิงบ้าง หาไปหามาก็เจอสองตัวเลือกคือ will_paginate และ paginator ซึ่งทั้งคู่เป็น gem ที่เราสามารถติดตั้งแล้วเอามาใช้ทดแทน paginate ใน Rails 2.0 ได้
เนื่องจากว่าในวิกิมีขั้นตอนแนะนำการติดตั้งอยู่ เลยลองใช้ will_paginate แทนดูซะ เพราะมีตัวอย่างอยู่ด้วย แรกๆลองใช้งานก็งงเล็กน้อย แต่ก็ถึงบางอ้อในเวลารวดเร็วเพราะใช้งานไม่ยากเลย คล้ายๆกับตัว paginate ที่มากับ Rails 2.0 แต่ง่ายกว่าซะด้วยซ้ำ
เจ้า will_paginate นี้จะทำการเพิ่ม method paginate ที่ทำงานได้เหมือนๆกับ find ของ ActiveRecord แค่คุณแทน find ด้วย paginate แล้วก็ส่ง argument ที่ method นี้ต้องการ ซึ่งก็คือเลขที่หน้าและจำนวนข้อมูลที่แสดงในแต่ละหน้า (ถ้าคุณไม่ระบุ will_paginate จะแสดง 30 record ในแต่ละหน้า)
ลองดูตัวอย่างทดแทน Listing 10.16 กัน:
def index ... if params[:id] @initial = params[:id] @specs = Spec.paginate(:conditions => ["last_name LIKE ?", @initial+'%'], : Read the rest of this entry »