Mock ใน RSpec คืออะไร?
(หรือ mock โดยทั่วไป เพราะไม่ได้เป็นคอนเซปต์เฉพาะ RSpec)
Mock คือออบเจกต์ที่ใช้สำหรับทดสอบ
คุณใช้ mocks เพื่อทดสอบการปฏิสัมพันธ์ระหว่างสองออบเจกต์ แทนที่จะทดสอบค่า output เหมือน expectation ปกติ
ตัวอย่าง:
คุณกำลังเขียน API ที่พลิกภาพ
แทนที่จะเขียนโค้ดจัดการภาพเอง คุณใช้ gem เช่น mini_magick
คุณอยากทดสอบการปฏิสัมพันธ์ระหว่างโค้ดของคุณกับ external dependency นี้… ดังนั้นคุณเขียน mock ที่คาดหวังว่าเมธอดที่ถูกต้องจะถูกเรียกบนคลาส ImageProcessor
นี่หมายความว่าคุณไม่ต้องพลิกภาพ (operation ช้า) ทุกครั้งที่รันเทสต์
มันทำงานอย่างไร?
Mocks แทนที่ออบเจกต์ต้นฉบับ ดังนั้นเมธอดจริงจะไม่ถูกเรียก
มาเริ่มตัวอย่างโค้ด!
ตัวอย่าง RSpec Mock
นี่คือเทสต์ ImageFlipper:
RSpec.describe "ImageFlipper" do
it "calls the flip method with the correct arguments" do
mock = double("mini_magick")
expect(mock).to receive(:flip).with("ruby.jpg")
img = ImageFlipper.new(mock)
img.flip("ruby.jpg")
end
end
ด้วยเทสต์นี้ เราสามารถเขียนโค้ดด้วย TDD
ก่อนอื่น:
เราต้องเขียนคลาส ImageFlipper
แบบนี้:
class ImageFlipper
def initialize(image_processor)
@image_processor = image_processor
end
end
เราต้องการเมธอด flip ด้วย:
def flip(file_name) end
ตอนนี้เราได้ feedback จาก RSpec:
Failures:
1) ImageFlipper calls the flip method with the correct arguments
Failure/Error: expect(mock).to receive(:flip).with("ruby.jpg")
(Double "mini_magick").flip("ruby.jpg")
expected: 1 time with arguments: ("ruby.jpg")
received: 0 times
# ./rspec-mocks.rb:6:in `block (2 levels) in <top (required)>'
ข้อความนี้บอกว่าเมธอด flip ถูกเรียก 0 ครั้ง แต่คาดหวัง 1 ครั้ง
คุณสามารถ ทำให้เทสต์ผ่านโดยให้มันสิ่งที่มันอยากได้:
def flip(file_name) @image_processor.flip(file_name) end
และนั่นคือ เทสต์ผ่านแล้ว:
. Finished in 0.00751 seconds (files took 0.11157 seconds to load) 1 example, 0 failures
มา Review: เราทำอะไรไป?
เราสร้างคลาส ImageFlipper ที่รับ image_processor
Processor นี้ตอบสนองต่อเมธอด flip
เราใช้ mock ทดสอบว่ามีการเรียกเมธอดนี้ 1 ครั้ง พร้อมอาร์กิวเมนต์
ผมรู้
ตัวอย่างนี้ง่าย
แต่คุณจินตนาการได้ถึงการนำไปปฏิบัติเต็มรูปแบบของ ImageFlipper ที่ตรวจสอบไฟล์มีอยู่, เป็นภาพถูกต้อง ฯลฯ
ความแตกต่างระหว่าง Mock และ Value Testing
ในเทสต์ปกติ คุณตรวจสอบ ค่าที่เมธอดคืน:
"เมธอดนี้คืนภาพที่พลิกแล้วไหม?"
เมื่อใช้ mock คุณทดสอบพฤติกรรม:
"เราสั่งให้ผู้อื่นทำสิ่งที่ถูกต้อง ด้วยข้อมูลที่ถูกต้อง และจำนวนครั้งที่แน่นอนที่เราต้องการไหม?"
Mocks เทียบกับ Stubs
จุดสับสนอีกอย่างคือการเปรียบเทียบ mocks และ stubs
ความแตกต่างคืออะไร?
- Stub คือเมธอดที่มี canned response ไม่สนใจพฤติกรรม
- Mock คาดหวังว่าเมธอดจะถูกเรียก หากไม่ถูกเรียกเทสต์จะ fail
ตัวอย่าง stub ใน RSpec:
stub = double("json")
allow(stub).to receive(:response) do
{"blog"=>"rubyguides.com", "rating"=>"5/5"}.to_json
end
เมธอด allow คือสิ่งที่ทำให้มันเป็น stub
เราอนุญาตให้ออบเจกต์ทดสอบ double("json") รับและตอบสนองต่อเมธอดนี้ แต่ เราไม่ตรวจสอบว่ามันถูกเรียก
นั่นคือความแตกต่าง!
วิธีใช้ Verified Doubles
ข้อเสียของ mocks และ stubs คือคุณอาจใช้เมธอดที่ไม่มีอยู่ใน production code
เพราะชื่อเมธอดเปลี่ยนไป… หรืออาจเป็น typo!
เจอ verified doubles
Verified double ใช้ได้ทั้งเป็น stub (allow) หรือ mock (expect) และจะตรวจสอบว่าเมธอดชื่อนี้มีอยู่จริง
ตัวอย่าง:
mock = instance_double(ImageProcessor)
จะ raise error หากเมธอดไม่มีอยู่:
1) ImageFlipper calls the flip method with the correct arguments
Failure/Error: expect(mock).to receive(:flip).with("ruby.jpg")
the ImageProcessor class does not implement the instance method: flip
แตทำงานถูกต้องหากเมธอดมีอยู่
Mocks ที่คืนค่า
กลับไปที่ mocking
ในตัวอย่างล่าสุด เรามี:
expect(mock).to receive(:flip).with("ruby.jpg")
เมื่อโค้ดของคุณเรียก flip mock จะคืน nil
หากโค้ดคาดหวังค่าอื่นนอกจาก nil จะเกิด error
คุณแก้ได้โดยทำให้ mock คืนผลลัพธ์
แบบนี้:
expect(mock).to receive(:flip).with("ruby.jpg").and_return("ruby-flipped.jpg")
วิธี Mock Instance Methods
สมมติว่าคุณมีโค้ดแบบนี้:
class NumberGenerator
def random
"A" * rand(1..10)
end
end
เมธอดนี้ยากต่อการทดสอบเพราะความสุ่ม
RSpec อนุญาตให้คุณ mock หรือ stub rand
แบบนี้:
it "generates a random number" do
generator = NumberGenerator.new
allow(generator).to receive(:rand).and_return(5)
expect(generator.random).to eq("AAAAA")
end
ตอนนี้:
rand คืนค่าคงที่ ดังนั้นคุณสามารถใช้ทดสอบผลลัพธ์ของเมธอดได้
ในทางที่ดีที่สุด คุณควร inject dependency (ฟังก์ชัน rand ในกรณีนี้) เพื่อควบคุมได้ Inject dependency หมายถึง คุณส่งมันเข้ามาเป็นพารามิเตอร์ ไม่ได้อะไร fancy
แตบางครั้งแค่ stub เมธอดก็สะดวกกว่า
เมื่อไหร่ควรใช้ Mocks?
ตอนนี้คำถามใหญ่…
พอดีเมื่อไหร่ควรใช้ mocks?
Software development เป็นหัวข้อซับซ้อน
แต่มี guidelines บางอย่างที่ปฏิบัติได้:
- หากเมธอดที่ทดสอบคืนค่า และไม่มี side effects (สร้างไฟล์, request API ฯลฯ) คุณไม่ต้องการ mock แค่ตรวจสอบ return value
- หากเมธอดทำงานกับ external objects และส่งคำสั่งไปพวกมัน คุณสามารถ mock interactions กับออบเจกต์เหล่านั้น
- หากเมธอด REQUEST ข้อมูลจาก external service (เช่น API) คุณสามารถใช้ stub เพื่อให้ข้อมูลสำหรับทดสอบ
คุณอยาก สงวน mocking สำหรับ interactions กับโลกภายนอก
กล่าวอีกนัยหนึ่ง…
หลีกเลี่ยงการ mock คลาสของแอปพลิเคชันตัวเอง!
ทำไม?
เพราะมันส่งเสริม coupling ระหว่างเทสต์กับ implementation details และ ทำให้โค้ดของคุณยากต่อการเปลี่ยนแปลง
ข้อยกเว้นคือคลาสที่เป็น wrapper สำหรับ 3rd party code
ดูวิดีอนี้เพื่อเรียนรู้เพิ่มเติม:
สรุป
คุณเรียนรู้เกี่ยวกับ RSpec mocks, stubs และ verified doubles แล้ว!
กรุณา แชร์บทความนี้ ให้ผู้คนอีกมากขึ้นได้เพลิดเพลินและได้ประโยชน์จากเนื้อหานี้
ขอบคุณที่อ่าน 🙂