ความแตกต่างระหว่าง Mock / Stub / Spy ในกรอบการทดสอบ Spock


103

ฉันไม่เข้าใจความแตกต่างระหว่างการทดสอบ Mock, Stub และ Spy ใน Spock และแบบฝึกหัดที่ฉันดูทางออนไลน์ไม่ได้อธิบายรายละเอียดไว้

คำตอบ:


97

ข้อควรระวัง: ฉันจะทำให้เข้าใจผิดมากเกินไปและอาจจะผิดพลาดเล็กน้อยในย่อหน้าที่กำลังจะมาถึง สำหรับข้อมูลเพิ่มเติมโปรดดูรายละเอียดเว็บไซต์ของมาร์ตินฟาวเลอร์

การจำลองคือคลาสดัมมี่ที่แทนที่ของจริงโดยส่งคืนค่าบางอย่างเช่น null หรือ 0 สำหรับการเรียกแต่ละวิธี คุณใช้การเยาะเย้ยหากคุณต้องการตัวอย่างจำลองของคลาสที่ซับซ้อนซึ่งอาจใช้ทรัพยากรภายนอกเช่นการเชื่อมต่อเครือข่ายไฟล์หรือฐานข้อมูลหรืออาจใช้วัตถุอื่น ๆ อีกมากมาย ข้อดีของการล้อเลียนคือคุณสามารถแยกชั้นเรียนที่อยู่ระหว่างการทดสอบออกจากระบบที่เหลือได้

ต้นขั้วยังเป็นคลาสจำลองที่ให้ผลลัพธ์ที่เล่นซ้ำที่เฉพาะเจาะจงมากขึ้นเตรียมหรือบันทึกไว้ล่วงหน้าสำหรับคำขอบางรายการที่อยู่ระหว่างการทดสอบ คุณสามารถพูดได้ว่าต้นขั้วเป็นการล้อเลียนที่แปลกใหม่ ใน Spock คุณมักจะอ่านเกี่ยวกับวิธีต้นขั้ว

สายลับเป็นชนิดของลูกผสมระหว่างวัตถุจริงและต้นขั้วกล่าวคือโดยพื้นฐานแล้วเป็นวัตถุจริงด้วยวิธีการบางอย่าง (ไม่ใช่ทั้งหมด) ที่ถูกบดบังด้วยวิธีต้นขั้ว เมธอดที่ไม่ถูกตัดออกจะถูกส่งผ่านไปยังวัตถุดั้งเดิม วิธีนี้จะทำให้คุณมีพฤติกรรมดั้งเดิมสำหรับวิธีการที่ "ถูก" หรือไม่สำคัญและพฤติกรรมปลอมสำหรับวิธี "ราคาแพง" หรือวิธีที่ซับซ้อน


อัปเดต 2017-02-06:คำตอบของผู้ใช้ mikhail นั้นเฉพาะเจาะจงกับ Spock มากกว่าคำตอบเดิมของฉันด้านบน ดังนั้นภายในขอบเขตของ Spock สิ่งที่เขาอธิบายนั้นถูกต้อง แต่นั่นไม่ได้ทำให้คำตอบทั่วไปของฉันผิด:

  • ต้นขั้วเกี่ยวข้องกับการจำลองพฤติกรรมเฉพาะ ใน Spock นี่คือต้นขั้วทั้งหมดที่ทำได้ดังนั้นจึงเป็นสิ่งที่ง่ายที่สุด
  • การเยาะเย้ยเกี่ยวข้องกับการยืนอยู่ในวัตถุจริง (อาจมีราคาแพง) โดยให้คำตอบที่ไม่ต้องใช้สำหรับการเรียกใช้เมธอดทั้งหมด ในเรื่องนี้การล้อเลียนนั้นง่ายกว่าต้นขั้ว แต่ใน Spock การเยาะเย้ยยังสามารถทำให้ผลลัพธ์ของวิธีการลอกเลียนแบบได้เช่นเป็นทั้งแบบจำลองและแบบต้นขั้ว นอกจากนี้ใน Spock เราสามารถนับได้ว่ามีการเรียกวิธีการจำลองที่เฉพาะเจาะจงกับพารามิเตอร์บางอย่างในระหว่างการทดสอบบ่อยเพียงใด
  • สายลับจะห่อหุ้มวัตถุจริงเสมอและโดยค่าเริ่มต้นจะกำหนดเส้นทางการเรียกใช้วิธีการทั้งหมดไปยังวัตถุดั้งเดิมและส่งผ่านผลลัพธ์ดั้งเดิมด้วย วิธีการนับการโทรยังใช้ได้กับสายลับ ใน Spock สายลับยังสามารถปรับเปลี่ยนพฤติกรรมของออบเจ็กต์ดั้งเดิมจัดการกับพารามิเตอร์การเรียกเมธอดและ / หรือผลลัพธ์หรือปิดกั้นเมธอดเดิมไม่ให้ถูกเรียกเลย

ต่อไปนี้คือตัวอย่างการทดสอบที่ปฏิบัติการได้ซึ่งแสดงให้เห็นว่าอะไรเป็นไปได้และอะไรไม่ได้ มันให้คำแนะนำมากกว่าตัวอย่างของ mikhail เล็กน้อย ขอบคุณมากที่เขาเป็นแรงบันดาลใจให้ฉันปรับปรุงคำตอบของตัวเอง! :-)

package de.scrum_master.stackoverflow

import org.spockframework.mock.TooFewInvocationsError
import org.spockframework.runtime.InvalidSpecException
import spock.lang.FailsWith
import spock.lang.Specification

class MockStubSpyTest extends Specification {

  static class Publisher {
    List<Subscriber> subscribers = new ArrayList<>()

    void addSubscriber(Subscriber subscriber) {
      subscribers.add(subscriber)
    }

    void send(String message) {
      for (Subscriber subscriber : subscribers)
        subscriber.receive(message);
    }
  }

  static interface Subscriber {
    String receive(String message)
  }

  static class MySubscriber implements Subscriber {
    @Override
    String receive(String message) {
      if (message ==~ /[A-Za-z ]+/)
        return "ok"
      return "uh-oh"
    }
  }

  Subscriber realSubscriber1 = new MySubscriber()
  Subscriber realSubscriber2 = new MySubscriber()
  Publisher publisher = new Publisher(subscribers: [realSubscriber1, realSubscriber2])

  def "Real objects can be tested normally"() {
    expect:
    realSubscriber1.receive("Hello subscribers") == "ok"
    realSubscriber1.receive("Anyone there?") == "uh-oh"
  }

  @FailsWith(TooFewInvocationsError)
  def "Real objects cannot have interactions"() {
    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then:
    2 * realSubscriber1.receive(_)
  }

  def "Stubs can simulate behaviour"() {
    given:
    def stubSubscriber = Stub(Subscriber) {
      receive(_) >>> ["hey", "ho"]
    }

    expect:
    stubSubscriber.receive("Hello subscribers") == "hey"
    stubSubscriber.receive("Anyone there?") == "ho"
    stubSubscriber.receive("What else?") == "ho"
  }

  @FailsWith(InvalidSpecException)
  def "Stubs cannot have interactions"() {
    given: "stubbed subscriber registered with publisher"
    def stubSubscriber = Stub(Subscriber) {
      receive(_) >> "hey"
    }
    publisher.addSubscriber(stubSubscriber)

    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then:
    2 * stubSubscriber.receive(_)
  }

  def "Mocks can simulate behaviour and have interactions"() {
    given:
    def mockSubscriber = Mock(Subscriber) {
      3 * receive(_) >>> ["hey", "ho"]
    }
    publisher.addSubscriber(mockSubscriber)

    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then: "check interactions"
    1 * mockSubscriber.receive("Hello subscribers")
    1 * mockSubscriber.receive("Anyone there?")

    and: "check behaviour exactly 3 times"
    mockSubscriber.receive("foo") == "hey"
    mockSubscriber.receive("bar") == "ho"
    mockSubscriber.receive("zot") == "ho"
  }

  def "Spies can have interactions"() {
    given:
    def spySubscriber = Spy(MySubscriber)
    publisher.addSubscriber(spySubscriber)

    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then: "check interactions"
    1 * spySubscriber.receive("Hello subscribers")
    1 * spySubscriber.receive("Anyone there?")

    and: "check behaviour for real object (a spy is not a mock!)"
    spySubscriber.receive("Hello subscribers") == "ok"
    spySubscriber.receive("Anyone there?") == "uh-oh"
  }

  def "Spies can modify behaviour and have interactions"() {
    given:
    def spyPublisher = Spy(Publisher) {
      send(_) >> { String message -> callRealMethodWithArgs("#" + message) }
    }
    def mockSubscriber = Mock(MySubscriber)
    spyPublisher.addSubscriber(mockSubscriber)

    when:
    spyPublisher.send("Hello subscribers")
    spyPublisher.send("Anyone there?")

    then: "check interactions"
    1 * mockSubscriber.receive("#Hello subscribers")
    1 * mockSubscriber.receive("#Anyone there?")
  }
}

ความแตกต่างระหว่างจำลองและต้นขั้วยังไม่ชัดเจนที่นี่ ด้วยการล้อเลียนเราต้องการตรวจสอบพฤติกรรม (ถ้าจะเรียกวิธีนี้กี่ครั้งและกี่ครั้ง) ด้วยต้นขั้วหนึ่งจะตรวจสอบสถานะเท่านั้น (เช่นขนาดของคอลเลกชันหลังการทดสอบ) FYI: mocks สามารถให้ผลลัพธ์ที่เตรียมไว้ได้เช่นกัน
chipiik

ขอบคุณ @mikhail และ chipiik สำหรับความคิดเห็นของคุณ ฉันได้อัปเดตคำตอบหวังว่าจะปรับปรุงและชี้แจงบางสิ่งที่ฉันเขียนในตอนแรก คำเตือน: ในคำตอบเดิมของฉันฉันบอกว่าฉันพูดเกินจริงและแอบอ้างข้อเท็จจริงที่เกี่ยวข้องกับสป็อคเล็กน้อย ฉันต้องการให้ผู้คนเข้าใจความแตกต่างพื้นฐานระหว่างการลอกเลียนแบบการล้อเลียนและการสอดแนม
kriegaex

@chipiik อีกสิ่งหนึ่งในการตอบกลับความคิดเห็นของคุณ: ฉันเป็นโค้ชทีมพัฒนามาหลายปีแล้วและเห็นพวกเขาใช้ Spock หรือ JUnit อื่น ๆ กับเฟรมเวิร์กจำลองอื่น ๆ ในกรณีส่วนใหญ่เมื่อใช้ล้อเลียนพวกเขาไม่ได้ทำเพื่อตรวจสอบพฤติกรรม (เช่นการเรียกใช้วิธีการนับ) แต่เพื่อแยกวัตถุที่ทดสอบออกจากสภาพแวดล้อม การนับการโต้ตอบ IMO เป็นเพียงส่วนเสริมและควรใช้อย่างรอบคอบและเท่าที่จำเป็นเนื่องจากมีแนวโน้มที่การทดสอบดังกล่าวจะแตกเมื่อทดสอบการเดินสายของส่วนประกอบมากกว่าพฤติกรรมที่แท้จริง
kriegaex

คำตอบสั้น ๆ แต่ยังมีประโยชน์มาก
Chaklader Asfak Arefe

54

คำถามอยู่ในบริบทของกรอบงาน Spock และฉันไม่เชื่อว่าคำตอบปัจจุบันคำนึงถึงสิ่งนี้

อ้างอิงจากเอกสาร Spock (ตัวอย่างที่กำหนดเองเพิ่มถ้อยคำของฉันเอง):

Stub: ใช้เพื่อให้ผู้ทำงานร่วมกันตอบสนองต่อวิธีการโทรในลักษณะหนึ่ง เมื่อขีดฆ่าเมธอดคุณจะไม่สนใจว่าเมธอดนั้นจะถูกเรียกใช้กี่ครั้งและกี่ครั้ง คุณแค่ต้องการให้มันคืนค่าบางอย่างหรือแสดงผลข้างเคียงบางอย่างเมื่อใดก็ตามที่ถูกเรียก

subscriber.receive(_) >> "ok" // subscriber is a Stub()

Mock: ใช้เพื่ออธิบายปฏิสัมพันธ์ระหว่างวัตถุภายใต้ข้อกำหนดและผู้ทำงานร่วมกัน

def "should send message to subscriber"() {
    when:
        publisher.send("hello")

    then:
        1 * subscriber.receive("hello") // subscriber is a Mock()
}

Mock สามารถทำหน้าที่เป็น Mock และ Stub:

1 * subscriber.receive("message1") >> "ok" // subscriber is a Mock()

Spy: มักจะขึ้นอยู่กับวัตถุจริงด้วยวิธีการดั้งเดิมที่ทำของจริง สามารถใช้เหมือน Stub เพื่อเปลี่ยนค่าการส่งคืนของวิธีการเลือก สามารถใช้เหมือน Mock เพื่ออธิบายการโต้ตอบ

def subscriber = Spy(SubscriberImpl, constructorArgs: ["Fred"])

def "should send message to subscriber"() {
    when:
        publisher.send("hello")

    then:
        1 * subscriber.receive("message1") >> "ok" // subscriber is a Spy(), used as a Mock an Stub
}

def "should send message to subscriber (actually handle 'receive')"() {
    when:
        publisher.send("hello")

    then:
        1 * subscriber.receive("message1") // subscriber is a Spy(), used as a Mock, uses real 'receive' function
}

สรุป:

  • Stub () คือ Stub
  • A Mock () คือ Stub and Mock
  • A Spy () คือ Stub, Mock และ Spy

หลีกเลี่ยงการใช้ Mock () หาก Stub () เพียงพอ

หลีกเลี่ยงการใช้ Spy () หากทำได้การทำเช่นนั้นอาจเป็นกลิ่นและคำแนะนำในการทดสอบที่ไม่ถูกต้องหรือการออกแบบวัตถุที่ไม่ถูกต้องภายใต้การทดสอบ


1
เพียงเพื่อเพิ่ม: อีกเหตุผลหนึ่งที่คุณต้องการลดการใช้ล้อเลียนให้น้อยที่สุดก็คือการล้อเลียนนั้นคล้ายกับการยืนยันมากเนื่องจากคุณตรวจสอบสิ่งต่างๆด้วยการเยาะเย้ยที่อาจล้มเหลวในการทดสอบและคุณต้องการลดจำนวนการตรวจสอบให้น้อยที่สุด คุณทำแบบทดสอบเพื่อให้การทดสอบมีสมาธิและเรียบง่าย ดังนั้นควรมีการจำลองเพียงครั้งเดียวต่อการทดสอบ
Sammi

2
"A Spy () คือ Stub, Mock และ Spy" นี่ไม่เป็นความจริงสำหรับสายลับ sinon?
basickarl

2
ฉันเพิ่งดูสายลับ Sinon อย่างรวดเร็วและดูเหมือนว่าพวกเขาไม่ได้ทำตัวเป็น Mocks หรือ Stubs โปรดทราบว่าคำถาม / คำตอบนี้อยู่ในบริบทของ Spock ซึ่งก็คือ Groovy ไม่ใช่ JS
mikhail

นี่ควรเป็นคำตอบที่ถูกต้องเนื่องจากขอบเขตนี้เป็นบริบทของ Spock นอกจากนี้การบอกว่าต้นขั้วเป็นรูปจำลองแฟนซีอาจทำให้เข้าใจผิดได้เนื่องจากการจำลองมีฟังก์ชันพิเศษ (การตรวจสอบจำนวนการเรียกใช้) ที่ต้นขั้วไม่ได้ (เยาะเย้ย> เพ้อฝันกว่าต้นขั้ว) อีกครั้งล้อเลียนและต้นขั้วตาม Spock
CGK

13

พูดง่ายๆคือ

จำลอง: คุณล้อเลียนประเภทและทันทีที่คุณได้รับวัตถุที่สร้างขึ้น วิธีการในวัตถุจำลองนี้จะส่งคืนค่าเริ่มต้นของประเภทการส่งคืน

Stub: คุณสร้างคลาส Stub ที่มีการกำหนดวิธีการใหม่ด้วยคำจำกัดความตามความต้องการของคุณ เช่นในวิธีออบเจ็กต์จริงคุณเรียกใช้และ API ภายนอกและส่งคืนชื่อผู้ใช้และ id ในเมธอดวัตถุที่ถูกดึงคุณจะส่งคืนชื่อจำลอง

Spy: คุณสร้างวัตถุจริงขึ้นมาหนึ่งชิ้นแล้วสอดแนม ตอนนี้คุณสามารถล้อเลียนวิธีการบางอย่างและเลือกที่จะไม่ทำสำหรับบางวิธี

ความแตกต่างในการใช้งานอย่างหนึ่งคือคุณไม่สามารถจำลองวัตถุระดับวิธีการได้ ในขณะที่คุณสามารถสร้างวัตถุเริ่มต้นในวิธีการแล้วสอดแนมเพื่อให้ได้พฤติกรรมที่ต้องการของวิธีการในวัตถุสอดแนม


0

Stubs เป็นเพียงเพื่ออำนวยความสะดวกในการทดสอบหน่วยเท่านั้น แต่ไม่ได้เป็นส่วนหนึ่งของการทดสอบ Mocks เป็นส่วนหนึ่งของการทดสอบส่วนหนึ่งของการตรวจสอบส่วนหนึ่งของการผ่าน / ไม่ผ่าน

สมมติว่าคุณมีวิธีการที่ใช้วัตถุเป็นพารามิเตอร์ คุณไม่เคยทำอะไรที่เปลี่ยนแปลงพารามิเตอร์นี้ในการทดสอบ คุณเพียงแค่อ่านค่าจากมัน นั่นคือต้นขั้ว

หากคุณเปลี่ยนแปลงสิ่งใดหรือต้องการตรวจสอบการโต้ตอบบางอย่างกับวัตถุแสดงว่าเป็นการเยาะเย้ย

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.