การล้อเลียนตัวแปรสมาชิกของคลาสโดยใช้ Mockito


142

ฉันเป็นมือใหม่ในการพัฒนาและการทดสอบหน่วยโดยเฉพาะ ฉันเดาว่าความต้องการของฉันค่อนข้างง่าย แต่ฉันอยากรู้ว่าคนอื่นคิดอย่างไรกับเรื่องนี้

สมมติว่าฉันมีสองคลาสเช่นนั้น -

public class First {

    Second second ;

    public First(){
        second = new Second();
    }

    public String doSecond(){
        return second.doSecond();
    }
}

class Second {

    public String doSecond(){
        return "Do Something";
    }
}

สมมติว่าฉันกำลังเขียน unit test เพื่อทดสอบFirst.doSecond()method อย่างไรก็ตามสมมติว่าฉันต้องการ Mock Second.doSecond()class เช่นนั้น ฉันใช้ Mockito เพื่อทำสิ่งนี้

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

ฉันเห็นว่าการเยาะเย้ยไม่เกิดผลและการยืนยันล้มเหลว ไม่มีวิธีล้อเลียนตัวแปรสมาชิกของคลาสที่ฉันต้องการทดสอบ เหรอ?

คำตอบ:


88

คุณต้องระบุวิธีการเข้าถึงตัวแปรสมาชิกเพื่อที่คุณจะสามารถส่งต่อได้ (วิธีทั่วไปส่วนใหญ่จะเป็นเมธอด setter หรือ constructor ซึ่งรับพารามิเตอร์)

หากโค้ดของคุณไม่มีวิธีดำเนินการแสดงว่ารหัสนั้นไม่ถูกต้องสำหรับ TDD (Test Driven Development)


4
ขอบคุณ. ฉันเห็นมัน. ฉันแค่สงสัยว่าฉันจะทำการทดสอบการรวมโดยใช้การจำลองได้อย่างไรซึ่งอาจมีวิธีการภายในมากมายคลาสที่อาจต้องล้อเลียน แต่ไม่จำเป็นต้องตั้งค่าผ่าน setXXX () ก่อนมือ
Anand Hemmige

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

9
เรียน @kittylyst ใช่อาจจะผิดจากมุมมองของ TDD หรือจากมุมมองที่มีเหตุผลใด ๆ แต่บางครั้งนักพัฒนาก็ทำงานในสถานที่ที่ไม่มีอะไรสมเหตุสมผลเลยและเป้าหมายเดียวที่มีก็เพียงแค่ทำเรื่องราวที่คุณมอบหมายให้เสร็จและจากไป ใช่มันไม่ถูกต้องมันไม่มีเหตุผลคนที่ไม่ผ่านการรับรองจะได้รับการตัดสินที่สำคัญและทุกสิ่งนั้น ดังนั้นในตอนท้ายของวันรูปแบบการต่อต้านจะชนะมาก
amanas

1
ฉันอยากรู้เกี่ยวกับเรื่องนี้ถ้าสมาชิกชั้นเรียนไม่มีเหตุผลที่จะถูกตั้งค่าจากภายนอกทำไมเราต้องสร้างเซ็ตเตอร์เพียงเพื่อจุดประสงค์ในการทดสอบ ลองนึกภาพคลาส 'Second' ในที่นี้คือตัวจัดการ FileSystem หรือเครื่องมือที่เริ่มต้นในระหว่างการสร้างวัตถุที่จะทดสอบ ฉันมีเหตุผลทั้งหมดที่ต้องการล้อเลียนตัวจัดการ FileSystem นี้เพื่อทดสอบชั้นหนึ่งและไม่มีเหตุผลที่จะทำให้เข้าถึงได้ ฉันทำได้ใน Python แล้วทำไมไม่ใช้ Mockito ล่ะ?
Zangdar

67

ไม่สามารถทำได้หากคุณไม่สามารถเปลี่ยนรหัสได้ แต่ฉันชอบการฉีดแบบพึ่งพาและ Mockito สนับสนุน:

public class First {    
    @Resource
    Second second;

    public First() {
        second = new Second();
    }

    public String doSecond() {
        return second.doSecond();
    }
}

การทดสอบของคุณ:

@RunWith(MockitoJUnitRunner.class)
public class YourTest {
   @Mock
   Second second;

   @InjectMocks
   First first = new First();

   public void testFirst(){
      when(second.doSecond()).thenReturn("Stubbed Second");
      assertEquals("Stubbed Second", first.doSecond());
   }
}

นี่เป็นสิ่งที่ดีและง่ายมาก


2
คิดว่าเป็นคำตอบที่ดีกว่าตัวอื่น ๆ เพราะ InjectMocks
sudocoder

เป็นเรื่องน่าตลกที่คน ๆ หนึ่งได้รับในฐานะมือใหม่ทดสอบอย่างตัวฉันเองที่จะไว้วางใจไลบรารีและเฟรมเวิร์กบางอย่าง ฉันคิดว่านี่เป็นเพียงความคิดที่ไม่ดีซึ่งบ่งบอกถึงความจำเป็นในการออกแบบใหม่ ... จนกว่าคุณจะแสดงให้ฉันเห็นว่ามันเป็นไปได้ (ชัดเจนและสะอาดมาก) ใน Mockito
หนูไมค์

9
@Resourceคืออะไร?
IgorGanapolsky

3
@IgorGanapolsky @ Resource คือคำอธิบายประกอบที่สร้าง / ใช้โดยกรอบงาน Java Spring วิธีที่บ่งบอกถึง Spring นี่คือถั่ว / วัตถุที่ Spring จัดการ stackoverflow.com/questions/4093504/resource-vs-autowired baeldung.com/spring-annotations-resource-inject-autowire ไม่ใช่สิ่งจำลอง แต่เนื่องจากใช้ในคลาสที่ไม่ใช่การทดสอบจึงต้องมีการล้อเลียนใน ทดสอบ.
Grez.Kev

ฉันไม่เข้าใจคำตอบนี้ คุณบอกว่ามันเป็นไปไม่ได้แล้วคุณแสดงว่ามันเป็นไปได้ไหม? สิ่งที่เป็นไปไม่ได้ที่นี่?
StackOverflowOfficial

35

หากคุณดูโค้ดของคุณอย่างใกล้ชิดคุณจะเห็นว่าsecondคุณสมบัติในการทดสอบของคุณยังคงเป็นเพียงตัวอย่างSecondไม่ใช่การเยาะเย้ย (คุณไม่ได้ผ่านการเยาะเย้ยfirstในรหัสของคุณ)

วิธีที่ง่ายที่สุดคือสร้าง setter สำหรับsecondในFirstชั้นเรียนและส่งต่อไปอย่างชัดเจน

แบบนี้:

public class First {

Second second ;

public First(){
    second = new Second();
}

public String doSecond(){
    return second.doSecond();
}

    public void setSecond(Second second) {
    this.second = second;
    }


}

class Second {

public String doSecond(){
    return "Do Something";
}
}

....

public void testFirst(){
Second sec = mock(Second.class);
when(sec.doSecond()).thenReturn("Stubbed Second");


First first = new First();
first.setSecond(sec)
assertEquals("Stubbed Second", first.doSecond());
}

อีกประการหนึ่งคือการส่งผ่านSecondอินสแตนซ์เป็นFirstพารามิเตอร์ตัวสร้าง

หากคุณไม่สามารถแก้ไขโค้ดได้ฉันคิดว่าตัวเลือกเดียวคือใช้การสะท้อน:

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");


    First first = new First();
    Field privateField = PrivateObject.class.
        getDeclaredField("second");

    privateField.setAccessible(true);

    privateField.set(first, sec);

    assertEquals("Stubbed Second", first.doSecond());
}

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


เข้าใจแล้ว ฉันอาจจะไปกับคำแนะนำแรกของคุณ
Anand Hemmige

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

@AnandHemmige อันที่สอง (ตัวสร้าง) นั้นสะอาดกว่าเนื่องจากหลีกเลี่ยงการสร้างอินสแตนซ์ `วินาทีที่ไม่แน่นอน ชั้นเรียนของคุณได้รับการแยกออกจากกันอย่างสวยงาม
soulcheck

10
Mockito มีคำอธิบายประกอบที่ดีเพื่อให้คุณฉีดม็อกของคุณลงในตัวแปรส่วนตัว ใส่คำอธิบายประกอบที่สองด้วย@Mockและใส่คำอธิบายประกอบ First ด้วย@InjectMocksและสร้างอินสแตนซ์ First ในตัวเริ่มต้น Mockito จะพยายามหาสถานที่ที่จะฉีด Second mock ลงในอินสแตนซ์แรกโดยอัตโนมัติรวมถึงการตั้งค่าฟิลด์ส่วนตัวที่ประเภทการจับคู่
jhericks

@Mockอยู่ที่ประมาณ 1.5 (อาจจะเร็วกว่านั้นฉันไม่แน่ใจ) 1.8.3 แนะนำ@InjectMocksเช่นเดียวกับ@Spyและ@Captor.
jhericks

7

หากคุณไม่สามารถเปลี่ยนตัวแปรสมาชิกได้วิธีอื่นก็คือใช้ powerMockit และโทร

Second second = mock(Second.class)
when(second.doSecond()).thenReturn("Stubbed Second");
whenNew(Second.class).withAnyArguments.thenReturn(second);

ตอนนี้ปัญหาคือการเรียกไปที่วินาทีใหม่ใด ๆ จะส่งคืนอินสแตนซ์ที่เยาะเย้ยเหมือนกัน แต่ในกรณีง่ายๆของคุณสิ่งนี้จะได้ผล


7

ฉันมีปัญหาเดียวกันกับที่ไม่ได้ตั้งค่าส่วนตัวเนื่องจาก Mockito ไม่เรียกตัวสร้างขั้นสูง นี่คือวิธีที่ฉันเพิ่มการล้อเลียนด้วยการสะท้อน

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

public class TestUtils {
    // get a static class value
    public static Object reflectValue(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(classToReflect, fieldNameValueToFetch);
            reflectField.setAccessible(true);
            Object reflectValue = reflectField.get(classToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // get an instance value
    public static Object reflectValue(Object objToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameValueToFetch);
            Object reflectValue = reflectField.get(objToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // find a field in the class tree
    public static Field reflectField(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField = null;
            Class<?> classForReflect = classToReflect;
            do {
                try {
                    reflectField = classForReflect.getDeclaredField(fieldNameValueToFetch);
                } catch (NoSuchFieldException e) {
                    classForReflect = classForReflect.getSuperclass();
                }
            } while (reflectField==null || classForReflect==null);
            reflectField.setAccessible(true);
            return reflectField;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch +" from "+ classToReflect);
        }
        return null;
    }
    // set a value with no setter
    public static void refectSetValue(Object objToReflect, String fieldNameToSet, Object valueToSet) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameToSet);
            reflectField.set(objToReflect, valueToSet);
        } catch (Exception e) {
            fail("Failed to reflectively set "+ fieldNameToSet +"="+ valueToSet);
        }
    }

}

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

@Test
public void testWithRectiveMock() throws Exception {
    // mock the base class using Mockito
    ClassToMock mock = Mockito.mock(ClassToMock.class);
    TestUtils.refectSetValue(mock, "privateVariable", "newValue");
    // and this does not prevent normal mocking
    Mockito.when(mock.somthingElse()).thenReturn("anotherThing");
    // ... then do your asserts
}

ฉันแก้ไขรหัสจากโครงการจริงของฉันที่นี่ในหน้า อาจมีปัญหาในการคอมไพล์หรือสองปัญหา ฉันคิดว่าคุณคงเข้าใจดี อย่าลังเลที่จะคว้ารหัสและใช้งานได้หากคุณพบว่ามีประโยชน์


คุณช่วยอธิบายรหัสของคุณด้วย usecase จริงได้ไหม เช่น Public class tobeMocker () {private ClassObject classObject; } โดยที่ classObject เท่ากับวัตถุที่จะถูกแทนที่
Jasper Lankhorst

ในตัวอย่างของคุณถ้า ToBeMocker instance = new ToBeMocker (); และ ClassObject someNewInstance = new ClassObject () {@Override // บางอย่างเช่นการพึ่งพาภายนอก}; จากนั้น TestUtils.refelctSetValue (เช่น "classObject", someNewInstance); โปรดทราบว่าคุณต้องหาสิ่งที่คุณต้องการลบล้างเพื่อล้อเลียน สมมติว่าคุณมีฐานข้อมูลและการแทนที่นี้จะส่งคืนค่าดังนั้นคุณไม่จำเป็นต้องเลือก ล่าสุดฉันมีรถบัสบริการซึ่งฉันไม่ต้องการดำเนินการกับข้อความจริง แต่ต้องการให้แน่ใจว่าได้รับแล้ว ดังนั้นฉันจึงตั้งค่าอินสแตนซ์รถบัสส่วนตัววิธีนี้
dave

คุณจะต้องจินตนาการว่ามีการจัดรูปแบบในความคิดเห็นนั้น มันถูกลบออก นอกจากนี้สิ่งนี้จะใช้ไม่ได้กับ Java 9 เนื่องจากจะล็อกการเข้าถึงส่วนตัว เราจะต้องทำงานร่วมกับโครงสร้างอื่น ๆ เมื่อเรามีการเปิดตัวอย่างเป็นทางการและสามารถทำงานร่วมกับขอบเขตที่แท้จริงได้
dave

1

คนอื่น ๆ หลายคนแนะนำให้คุณคิดทบทวนโค้ดของคุณใหม่เพื่อให้ทดสอบได้มากขึ้น - คำแนะนำที่ดีและมักจะง่ายกว่าที่ฉันกำลังจะแนะนำ

หากคุณไม่สามารถเปลี่ยนรหัสเพื่อให้ทดสอบได้มากขึ้น PowerMock: https://code.google.com/p/powermock/

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

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

@RunWith(PowerMockRunner.class)
@PrepareForTest({First.class})

จากนั้นในการตั้งค่าการทดสอบของคุณคุณสามารถใช้เมธอด whenNew เพื่อให้ตัวสร้างส่งคืนการจำลอง

whenNew(Second.class).withAnyArguments().thenReturn(mock(Second.class));

0

ใช่สิ่งนี้สามารถทำได้ดังที่การทดสอบต่อไปนี้แสดง (เขียนด้วย JMockit mocking API ซึ่งฉันพัฒนา):

@Test
public void testFirst(@Mocked final Second sec) {
    new NonStrictExpectations() {{ sec.doSecond(); result = "Stubbed Second"; }};

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

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


3
คำถามไม่ใช่ว่า JMockit ดีกว่า Mockito หรือไม่ แต่จะทำอย่างไรใน Mockito ยึดติดกับการสร้างผลิตภัณฑ์ที่ดีกว่าแทนที่จะมองหาโอกาสที่จะทำลายการแข่งขัน!
TheZuck

8
ผู้โพสต์ต้นฉบับบอกเพียงว่าเขาใช้ Mockito; เป็นเพียงการบอกเป็นนัยว่า Mockito เป็นข้อกำหนดที่ตายตัวและยากดังนั้นคำใบ้ที่ JMockit สามารถจัดการกับสถานการณ์นี้ได้จึงไม่เหมาะสม
Bombe

0

หากคุณต้องการทางเลือกอื่นแทนReflectionTestUtilsจาก Spring ใน mockito ให้ใช้

Whitebox.setInternalState(first, "second", sec);

ยินดีต้อนรับสู่ Stack Overflow! มีคำตอบอื่น ๆ ที่เป็นคำถามของ OP และถูกโพสต์เมื่อหลายปีก่อน เมื่อโพสต์คำตอบโปรดตรวจสอบว่าคุณได้เพิ่มโซลูชันใหม่หรือคำอธิบายที่ดีขึ้นมากโดยเฉพาะอย่างยิ่งเมื่อตอบคำถามเก่า ๆ หรือแสดงความคิดเห็นเกี่ยวกับคำตอบอื่น ๆ
help-info.de

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