ใช้ Mockito เพื่อจำลองวิธีการ แต่ไม่ใช่วิธีอื่น


402

มีวิธีใดบ้างที่ใช้ Mockito เพื่อเยาะเย้ยวิธีการบางอย่างในชั้นเรียน แต่ไม่ใช่วิธีอื่น ๆ ?

ตัวอย่างเช่นในStockชั้นเรียนนี้ (ประดิษฐ์ขึ้นแล้ว) ฉันต้องการจำลองgetPrice()และgetQuantity()คืนค่า (ตามที่แสดงในตัวอย่างการทดสอบด้านล่าง) แต่ฉันต้องการgetValue()ให้ทำการคูณตามรหัสในStockชั้นเรียน

public class Stock {
  private final double price;
  private final int quantity;

  Stock(double price, int quantity) {
    this.price = price;
    this.quantity = quantity;
  }

  public double getPrice() {
    return price;
  }

  public int getQuantity() {
    return quantity;
  }
  public double getValue() {
    return getPrice() * getQuantity();
  }

  @Test
  public void getValueTest() {
    Stock stock = mock(Stock.class);
    when(stock.getPrice()).thenReturn(100.00);
    when(stock.getQuantity()).thenReturn(200);
    double value = stock.getValue();
    // Unfortunately the following assert fails, because the mock Stock getValue() method does not perform the Stock.getValue() calculation code.
    assertEquals("Stock value not correct", 100.00*200, value, .00001);
}

4
ทำไมคุณต้องการทำเช่นนั้น? คุณควรจะทดสอบคลาส (ในกรณีนี้ไม่ควรเยาะเย้ย) หรือคุณควรเยาะเย้ยขณะทดสอบคลาสอื่น (ในกรณีนี้ไม่มีฟังก์ชัน) ทำไมคุณต้องเยาะเย้ยบางส่วน?
weltraumpirat

3
ตกลงนี่เป็นตัวอย่างเล็ก ๆ ของจริง ในความเป็นจริงฉันกำลังพยายามหลีกเลี่ยงการโทรไปยังฐานข้อมูลโดยผ่านค่าที่ประดิษฐ์ไว้ แต่ฉันต้องการตรวจสอบว่าวิธีอื่นทำงานอย่างถูกต้องกับค่าที่ประดิษฐ์ มีวิธีที่ดีกว่าในการทำเช่นนี้?
Victor Grazi

5
แน่นอน: ย้ายการเรียกฐานข้อมูลของคุณไปยังคลาสที่แยกต่างหาก (ตรรกะโดเมนและการเข้าถึงฐานข้อมูลไม่ควรอยู่ในระดับเดียวกันพวกเขาเป็นสองข้อกังวลที่แตกต่างกัน) แยกอินเทอร์เฟซใช้อินเทอร์เฟซนั้นเพื่อเชื่อมต่อ อินเตอร์เฟซในระหว่างการทดสอบ
weltraumpirat

1
ฉันเห็นด้วยอย่างสมบูรณ์มันเป็นการยากที่จะอธิบายภาพรวมทั้งหมดโดยไม่ต้องอัพโหลดรหัสที่นี่รวมถึงห้องสมุดบุคคลที่สาม
Victor Grazi

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

คำตอบ:


643

เพื่อตอบคำถามของคุณโดยตรงคุณสามารถเยาะเย้ยวิธีการบางอย่างได้โดยไม่ต้องเลียนแบบคนอื่น นี้เรียกว่าจำลองบางส่วน ดูเอกสารประกอบของ Mockito เกี่ยวกับ mocks บางส่วนสำหรับข้อมูลเพิ่มเติม

สำหรับตัวอย่างของคุณคุณสามารถทำสิ่งต่อไปนี้ในการทดสอบของคุณ:

Stock stock = mock(Stock.class);
when(stock.getPrice()).thenReturn(100.00);    // Mock implementation
when(stock.getQuantity()).thenReturn(200);    // Mock implementation
when(stock.getValue()).thenCallRealMethod();  // Real implementation

ในกรณีนั้นการใช้งานแต่ละวิธีจะถูกเยาะเย้ยเว้นแต่จะระบุthenCallRealMethod()ไว้ในwhen(..)ข้อ

นอกจากนี้ยังมีความเป็นไปได้ในทางกลับกันด้วยสายลับแทนที่จะเยาะเย้ย :

Stock stock = spy(Stock.class);
when(stock.getPrice()).thenReturn(100.00);    // Mock implementation
when(stock.getQuantity()).thenReturn(200);    // Mock implementation
// All other method call will use the real implementations

ในกรณีที่การดำเนินการทุกวิธีการที่เป็นจริง when(..)แต่ถ้าคุณได้กำหนดพฤติกรรมเยาะเย้ยด้วย

มีข้อผิดพลาดที่สำคัญอย่างหนึ่งเมื่อคุณใช้when(Object)กับสายลับเหมือนในตัวอย่างก่อนหน้านี้ วิธีการจริงจะถูกเรียก (เพราะstock.getPrice()มีการประเมินก่อนwhen(..)ที่รันไทม์) นี่อาจเป็นปัญหาหากวิธีการของคุณมีตรรกะที่ไม่ควรเรียก คุณสามารถเขียนตัวอย่างก่อนหน้านี้เช่นนี้:

Stock stock = spy(Stock.class);
doReturn(100.00).when(stock).getPrice();    // Mock implementation
doReturn(200).when(stock).getQuantity();    // Mock implementation
// All other method call will use the real implementations

ความเป็นไปได้อื่นอาจจะใช้org.mockito.Mockito.CALLS_REAL_METHODSเช่น:

Stock MOCK_STOCK = Mockito.mock( Stock.class, CALLS_REAL_METHODS );

ผู้ได้รับมอบหมายนี้โทร unstubbed เพื่อการใช้งานจริง


อย่างไรก็ตามด้วยตัวอย่างของคุณฉันเชื่อว่ามันจะยังคงล้มเหลวเนื่องจากการดำเนินการgetValue()อาศัยquantityและpriceมากกว่าgetQuantity()และgetPrice()ซึ่งเป็นสิ่งที่คุณได้เย้ยหยัน

ความเป็นไปได้อีกอย่างหนึ่งคือการหลีกเลี่ยง mocks ทั้งหมด:

@Test
public void getValueTest() {
    Stock stock = new Stock(100.00, 200);
    double value = stock.getValue();
    assertEquals("Stock value not correct", 100.00*200, value, .00001);
}

21
ฉันคิดว่าคำตอบนี้ผิด คุณต้อง SPY อินสแตนซ์ของวัตถุไม่ใช่เพื่อจำลองชั้นเรียน
GaRRaPeTa

2
@GaRRaPeTa ฉันจะบอกว่าการสอดแนมและการเยาะเย้ยเป็นทั้งทางเลือกที่สมเหตุสมผล เป็นการยากที่จะบอกว่ากรณีใดดีที่สุดสำหรับกรณีนี้เนื่องจาก OP ระบุว่านี่เป็นตัวอย่างที่ง่าย
Jon Newmuis

1
ไม่ควรเป็น "Spy" แทนที่จะเป็น "Mock" เนื่องจากเป็น "cabe" ที่เยาะเย้ยส่วนหนึ่งโดยวิธีที่ดีกว่า
Tarun Sapra

2
Stock stock = spy(Stock.class);ดูเหมือนว่าผิดspyวิธีดูเหมือนว่าจะยอมรับเฉพาะวัตถุที่ไม่ได้เรียน
Paramvir Singh Karwal

4
+1 สำหรับการชี้ให้เห็นความแตกต่างระหว่างdoReturn(retval).when(spyObj).methodName(args)และwhen(spyObj.methodName(args)).thenReturn(retval)
Captain_Obident

140

การเยาะเย้ยบางส่วนของคลาสได้รับการสนับสนุนผ่านSpyใน mockito

List list = new LinkedList();
List spy = spy(list);

//optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);

//using the spy calls real methods
spy.add("one");
spy.add("two");

//size() method was stubbed - 100 is printed
System.out.println(spy.size());

ตรวจสอบ1.10.19และ2.7.22เอกสารสำหรับคำอธิบายโดยละเอียด


37

ตามเอกสาร :

Foo mock = mock(Foo.class, CALLS_REAL_METHODS);

// this calls the real implementation of Foo.getSomething()
value = mock.getSomething();

when(mock.getSomething()).thenReturn(fakeValue);

// now fakeValue is returned
value = mock.getSomething();

2
ขอบคุณสำหรับการสาธิตวิธีการตั้งค่าการเยาะเย้ยที่เรียกใช้งานจริงสำหรับวิธีการทั้งหมดยกเว้นบางอย่างที่ฉันต้องควบคุมจากการทดสอบ
bigh_29

class NaughtyLinkedList extends LinkedList { public int size() { throw new RuntimeException("don't call me");} } @Test public void partialMockNaughtLinkedList(){ List mock = mock(NaughtyLinkedList.class, CALLS_REAL_METHODS); mock.add(new Object()); // this calls the real function when(mock.size()).thenReturn(2); // For whatever reason, this lines throws the RuntimeException. assertEquals(2,mock.size()); }มันใช้งานไม่ได้ ไม่ว่าจะด้วยเหตุผลใดก็ตามเมื่อ "เมื่อ" มีการดำเนินการจริงจะดำเนินการตามวิธีการที่ควรจะเยาะเย้ย รหัส:
Lance ชนิด

3
ปัญหาคือ "เมื่อ" "เมื่อ" จริงจะดำเนินการสิ่งที่คุณต้องการที่จะเยาะเย้ยบางส่วน เพื่อหลีกเลี่ยงปัญหานี้มีทางเลือกอื่น: doReturn () ดู doReturn () ได้ที่docs.mockito.googlecode.com/hg/1.9.5/org/mockito/ …
Lance Kind

18

สิ่งที่คุณต้องการเป็นorg.mockito.Mockito.CALLS_REAL_METHODSไปตามเอกสาร:

/**
 * Optional <code>Answer</code> to be used with {@link Mockito#mock(Class, Answer)}
 * <p>
 * {@link Answer} can be used to define the return values of unstubbed invocations.
 * <p>
 * This implementation can be helpful when working with legacy code.
 * When this implementation is used, unstubbed methods will delegate to the real implementation.
 * This is a way to create a partial mock object that calls real methods by default.
 * <p>
 * As usual you are going to read <b>the partial mock warning</b>:
 * Object oriented programming is more less tackling complexity by dividing the complexity into separate, specific, SRPy objects.
 * How does partial mock fit into this paradigm? Well, it just doesn't... 
 * Partial mock usually means that the complexity has been moved to a different method on the same object.
 * In most cases, this is not the way you want to design your application.
 * <p>
 * However, there are rare cases when partial mocks come handy: 
 * dealing with code you cannot change easily (3rd party interfaces, interim refactoring of legacy code etc.)
 * However, I wouldn't use partial mocks for new, test-driven & well-designed code.
 * <p>
 * Example:
 * <pre class="code"><code class="java">
 * Foo mock = mock(Foo.class, CALLS_REAL_METHODS);
 *
 * // this calls the real implementation of Foo.getSomething()
 * value = mock.getSomething();
 *
 * when(mock.getSomething()).thenReturn(fakeValue);
 *
 * // now fakeValue is returned
 * value = mock.getSomething();
 * </code></pre>
 */

ดังนั้นรหัสของคุณควรมีลักษณะดังนี้:

import org.junit.Test;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

public class StockTest {

    public class Stock {
        private final double price;
        private final int quantity;

        Stock(double price, int quantity) {
            this.price = price;
            this.quantity = quantity;
        }

        public double getPrice() {
            return price;
        }

        public int getQuantity() {
            return quantity;
        }

        public double getValue() {
            return getPrice() * getQuantity();
        }
    }

    @Test
    public void getValueTest() {
        Stock stock = mock(Stock.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
        when(stock.getPrice()).thenReturn(100.00);
        when(stock.getQuantity()).thenReturn(200);
        double value = stock.getValue();

        assertEquals("Stock value not correct", 100.00 * 200, value, .00001);
    }
}

การโทรเพื่อรับStock stock = mock(Stock.class);สายorg.mockito.Mockito.mock(Class<T>)ซึ่งมีลักษณะดังนี้:

 public static <T> T mock(Class<T> classToMock) {
    return mock(classToMock, withSettings().defaultAnswer(RETURNS_DEFAULTS));
}

เอกสารที่มีค่าRETURNS_DEFAULTSบอก:

/**
 * The default <code>Answer</code> of every mock <b>if</b> the mock was not stubbed.
 * Typically it just returns some empty value. 
 * <p>
 * {@link Answer} can be used to define the return values of unstubbed invocations. 
 * <p>
 * This implementation first tries the global configuration. 
 * If there is no global configuration then it uses {@link ReturnsEmptyValues} (returns zeros, empty collections, nulls, etc.)
 */

1
ด่างดี ... แต่ฉันจะถามได้ไหมว่าทำไมคุณถึงใช้withSettings()...แบบนั้น? ดูเหมือนว่าorg.mockito.internal.stubbing.answers.CallsRealMethods()(ตัวอย่าง) อาจทำงาน ... และ javadoc สำหรับชั้นเรียนนี้โดยเฉพาะกล่าวว่ามันใช้สำหรับ mocks บางส่วน ...
mike rodent

3
นอกจากนี้ ... จะไม่ทำงานนี้เป็นปัญหาที่พบโดยคำตอบอื่น ๆ ที่นี่: คือthenReturnจริงจะดำเนินการวิธีการ (ซึ่งอาจจะทำให้เกิดปัญหาแม้จะไม่ได้อยู่ในตัวอย่างนี้) และเพื่อให้doReturnเป็นที่นิยมในกรณีเช่นนี้ ... ?
ไมค์หนู

4

การเยาะเย้ยบางส่วนโดยใช้วิธีการสอดแนมของ Mockito อาจเป็นวิธีการแก้ปัญหาของคุณตามที่ระบุไว้แล้วในคำตอบข้างต้น ในระดับหนึ่งฉันเห็นด้วยว่าสำหรับกรณีการใช้งานที่เป็นรูปธรรมของคุณอาจเป็นการเหมาะสมที่จะเยาะเย้ยการค้นหา DB จากประสบการณ์ของฉันสิ่งนี้ไม่สามารถทำได้เสมอไป - อย่างน้อยก็ไม่มีวิธีแก้ไขปัญหาอื่น ๆ - ที่ฉันคิดว่าเป็นเรื่องยุ่งยากหรืออย่างน้อยก็เปราะบาง โปรดทราบว่าการเยาะเย้ยบางส่วนไม่สามารถใช้ได้กับ Mockito รุ่นพันธมิตร คุณใช้อย่างน้อย 1.8.0

ฉันเพิ่งจะเขียนความคิดเห็นแบบง่ายสำหรับคำถามต้นฉบับแทนที่จะโพสต์คำตอบนี้ แต่ StackOverflow ไม่อนุญาตสิ่งนี้

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


2
ฉันรู้สึกว่าคำตอบนี้เป็นความเห็นบางส่วน โปรดพิจารณาแก้ไข
soundslikeodd

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