Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เชี่ยวชาญ RSpec Mocks: คู่มือครบวงจรแบบขั้นตอน

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 บางอย่างที่ปฏิบัติได้:

  1. หากเมธอดที่ทดสอบคืนค่า และไม่มี side effects (สร้างไฟล์, request API ฯลฯ) คุณไม่ต้องการ mock แค่ตรวจสอบ return value
  2. หากเมธอดทำงานกับ external objects และส่งคำสั่งไปพวกมัน คุณสามารถ mock interactions กับออบเจกต์เหล่านั้น
  3. หากเมธอด REQUEST ข้อมูลจาก external service (เช่น API) คุณสามารถใช้ stub เพื่อให้ข้อมูลสำหรับทดสอบ

คุณอยาก สงวน mocking สำหรับ interactions กับโลกภายนอก

กล่าวอีกนัยหนึ่ง…

หลีกเลี่ยงการ mock คลาสของแอปพลิเคชันตัวเอง!

ทำไม?

เพราะมันส่งเสริม coupling ระหว่างเทสต์กับ implementation details และ ทำให้โค้ดของคุณยากต่อการเปลี่ยนแปลง

ข้อยกเว้นคือคลาสที่เป็น wrapper สำหรับ 3rd party code

ดูวิดีอนี้เพื่อเรียนรู้เพิ่มเติม:

สรุป

คุณเรียนรู้เกี่ยวกับ RSpec mocks, stubs และ verified doubles แล้ว!

กรุณา แชร์บทความนี้ ให้ผู้คนอีกมากขึ้นได้เพลิดเพลินและได้ประโยชน์จากเนื้อหานี้

ขอบคุณที่อ่าน 🙂