อยากเรียนรู้เขียน tests สำหรับ Ruby applications ด้วย RSpec?
แล้ว คุณอยู่ที่ถูกต้อง!
ใน tutorial นี้ ผมจะ show วิธีทำ
สารบัญ
- 1 ทำไมควรเขียน Tests?
- 1.0.1 สร้าง safety net ต่อ errors (มีประโยชน์มากสำหรับ refactoring)
- 1.0.2 ช่วย document โค้ด
- 1.0.3 ให้ feedback loop
- 1.0.4 ช่วย ensure โค้ดผลลัพธ์ที่คาดหวัง
- 1.0.5 ช่วยได้ Ruby job
- 2 Getting Started With RSpec
- 3 RSpec Testing Example
- 4 RSpec Let Method
- 5 วิธีใช้ Subject Method
- 6 วิธี Run Code ก่อนทุก Tests
- 7 วิธีสร้าง Testing Subgroups
- 8 วิธี Disable Test ชั่วคราว
- 9 รัน Examples ด้วย Name
- 10 RSpec Expectations & Matchers
- 11 RSpec Formatters
- 12 วิธีหา Slow Tests
- 13 RSpec Video Tutorial
- 14 สรุป
- 14.1 Related
ทำไมควรเขียน Tests?
นี่คือเหตุผล:
สร้าง safety net ต่อ errors (มีประโยชน์มากสำหรับ refactoring)
หากไม่มี test suite คุณไม่อยากแตะโค้ด เพราะกลัวพัง…
…มี tests เพิ่ม confidence!
ช่วย document โค้ด
Tests ของคุณ describe ว่า application ควรทำอะไร
ให้ feedback loop
ตอนทำ TDD คุณได้ feedback loop บอกว่าต้อง focus อะไรต่อ useful หาก distract ง่าย
ช่วย ensure โค้ดผลลัพธ์ที่คาดหวัง
นี่สำคัญ!
หากเขียน complex logic อยากแน่ใจว่าทำงานกับหลาย inputs ไม่ใช่แค่ตัวอย่างเดียว
Tests ช่วย uncover corner cases และ document พวกมัน
ช่วยได้ Ruby job
Job applications ส่วนใหญ่ appreciate testing skills เพิ่มโอกาสได้งาน
Getting Started With RSpec
เข้าใจ RSpec ทำงานอย่างไร มาไป step-by-step
เราจะเขียน application หา factorial numbers
ขั้นแรก:
require 'rspec/autorun' describe Factorial do # ... end
นี่คือ initial code สำหรับเขียน RSpec test แรก
ต้อง require rspec gem
จากนั้นสร้าง describe block เพื่อ group tests ทั้งหมด และบอก RSpec ว่าทดสอบคลาสไหน
ต่อไปคือ it block:
describe Factorial do
it "does something" do
# ...
end
end
นี่คือ test name บวกวิธี group components ของ test
Components:
- Setup
- Exercise
- Verify
Setup คือที่ สร้าง objects ที่ต้องการ
เป็น preparation phase
จากนั้นเรียกเมธอดที่อยาก exercise เพื่อได้ return value
สุดท้าย verify ผลลัพธ์ด้วย expectation (RSpec) หรือ assertion (Minitest)
RSpec Testing Example
ตอนนี้หากอยากเขียน factorial method ต้องหา valid values ออนไลน์หรือคำนวณมือ
ใช้กับ tests
แบบนี้:
describe Factorial do
it "finds the factorial of 5" do
calculator = Factorial.new
expect(calculator.factorial_of(5)).to eq(120)
end
end
เวลารันโค้ด (เหมือน Ruby program ปกติ) ได้:
uninitialized constant Factorial (NameError)
ปกติเพราะยังไม่มีคลาส Factorial
มาสร้าง:
class Factorial end
Error ถัดไป:
undefined method 'factorial_of'
แก้โดยสร้างเมธอด factorial_of:
class Factorial def factorial_of end end
รันอีกครั้ง:
wrong number of arguments (given 1, expected 0)
Error อีกครั้ง! แตคุณรู้ไหม?
นั่นดีมาก 🙂
Errors ไม่ใช่สิ่ง frustrating
มันคือ feedback
ตอนนี้:
เพิ่ม method argument ให้ factorial_of:
class Factorial def factorial_of(n) end end
ตอนนี้ได้ test failure:
expected: 120 got: nil
นี่คือจุดที่คุณอยากอยู่!
งานต่อไป implement method:
class Factorial
def factorial_of(n)
(1..n).inject(:*)
end
end
และได้ first passing test:
. Finished in 0.00315 seconds (files took 0.09083 seconds to load) 1 example, 0 failures
นี่คือ test-driven development (TDD)
เขียน tests ก่อน ให้ tests guide ว่าต้องทำอะไรต่อ
RSpec Let Method
หากอยากเขียนหลาย tests และ reuse objects เดียวกัน นิยาม objects ด้วย let statements
长这样:
describe Factorial do
let(:calculator) { Factorial.new }
it "finds the factorial of 5" do
expect(calculator.factorial_of(5)).to eq(120)
end
end
ตอนนี้ reuse calculator ในทุก tests ภายใต้ describe block เดียวกัน
สิ่งหนึ่งเกี่ยว let คือมัน "lazy"
หมายถึงอะไร?
Object ไม่สร้างจนกว่าจะใช้ครั้งแรก
มีผลหากการสร้าง object มี side-effects เช่นสร้าง database entries หรือเขียนไฟล์
ดีที่สุดหลีกเลี่ยง side-effects แตหากไม่ได้ ใช้ let!
ตัวอย่าง:
let!(:user) { User.create("rspec@rubyguides.com") }
let! non-lazy object สร้างก่อนรัน tests
วิธีใช้ Subject Method
Version อื่นของ let คือ subject
ความแตกต่างคือมี subject ได้หนึ่งตัว และ meant to be instance ของ main object ที่ทดสอบ
RSpec สร้าง default subject แบบนี้:
subject { Factorial.new }
เรียกว่า "implicit subject"
ใช้ได้แบบนี้:
describe Factorial do
it "finds the factorial of 5" do
expect(subject.factorial_of(5)).to eq(120)
end
end
ตั้งชื่อ subject ได้:
subject(:calculator) { Factorial.new }
พฤติกรรมเหมือน let แต enable one-line expectations:
it { should be_empty }
วิธี Run Code ก่อนทุก Tests
RSpec มี execution hooks ใช้ run something ก่อนและหลังทุก test หรือ group
ตัวอย่าง:
describe Shop do
before(:all) { Shop.prepare_database }
after (:all) { Shop.cleanup_database }
end
หากอยากรัน per example (example = test ใน RSpec) ใช้ :each แทน :all
วิธีสร้าง Testing Subgroups
หากทดสอบ scenarios ต่างๆ ใน app อาจช่วย group related tests
ทำได้ด้วย context block ใน RSpec
ตัวอย่าง:
describe Course do
context "when user is logged in" do
it "displays the course lessons" do end
it "displays the course description" do end
end
context "when user it NOT logged in" do
it "redirects to login page" do end
it "it shows a message" do end
end
end
วิธี Disable Test ชั่วคราว
Disable test ได้สำหรับ debugging
แค่เปลี่ยน it เป็น xit สำหรับ tests ที่อยาก disable
ตัวอย่าง:
xit "eats lots of bacon" do end
อย่าลืมลบ x ตอนเสร็จ!
รัน Examples ด้วย Name
แทน disable tests คุณ filter tests ที่อยากรันด้วย flag -e
ตัวอย่าง:
> ruby person.rb -e bacon
Filtering based on test name ดังนั้น above matches tests ที่มีคำ "bacon"
RSpec Expectations & Matchers
คุณจำตัวอย่างที่ใช้อยู่?
expect(calculator.factorial_of(5)).to eq(120)
แต eq(120) ส่วนนี้คืออะไร?
120 คือค่าที่คาดหวัง…
และ eq คือ matcher
Matchers คือวิธี RSpec compare output ของเมธอด กับ expected value
กรณี eq RSpec ใช้ operator == (อ่านเพิ่มเกี่ยวกับ Ruby operators)
แต มี matchers อื่นๆ ใช้ได้
เช่น be_something matcher:
expect(nil).to be_nil
ที่ something คือ predicate method (เช่น empty?) เรียกบน test results
Matchers useful อื่นๆ:
- include (สำหรับ arrays และ hashes)
- start_with
- end_with
- match (สำหรับ regex matching)
- be_between
- have_key / have_value (สำหรับ hashes)
- be_instance_of
- respond_to
- have_attributes (สำหรับทดสอบ instance variables)
Matcher ที่ต้อง ระวังพิเศษ คือ raise_error
เหตุผลคือต้อง wrap expectation ใน block
แบบนี้:
expect{ :x.count }.to raise_error(NoMethodError)
change matcher ทำงานแบบเดียวกัน:
expect{ stock.increment }.to change(stock, :value).by(100)
RSpec Formatters
Default RSpec output เป็น format "progress"
Format นี้เห็น dots (.) represent 1 passing test, F สำหรับ failed test, E สำหรับ error
แตมี alternative formatting options
รายการ:
- progress
- documentation
- json
- html
Enable ด้วย flag -f:
> ruby factorial.rb -f d Person eats lots of healthy food writes many articles Finished in 0.00154 seconds (files took 0.09898 seconds to load) 2 examples, 0 failures
Documentation format ใช้ test descriptions generate output
วิธีหา Slow Tests
RSpec มี option handy profile tests
แค่ส่ง flag --profile จะเห็นแต่ละ test ใช้เวลานานแค่ไหน และ fix slow ones
ตัวอย่าง:
> ruby factorial.rb --profile Factorial finds the factorial of 5 0.00043 seconds
RSpec Video Tutorial
สรุป
คุณเรียนรู้เขียน tests ด้วย RSpec testing framework แล้ว!
ตอนถึงคุณเริ่มเขียน test ของตัวเอง!