นี่เป็นการใช้วิธีรีเซ็ตที่เหมาะสมของ Mockito หรือไม่


68

ฉันมีวิธีส่วนตัวในชั้นทดสอบของฉันที่สร้างBarวัตถุที่ใช้กันทั่วไป คอนBarสตรัคsomeMethod()วิธีการโทรในวัตถุที่เยาะเย้ยของฉัน:

private @Mock Foo mockedObject; // My mocked object
...

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
}

ในบางวิธีการทดสอบของฉันฉันต้องการตรวจสอบsomeMethodก็ถูกเรียกใช้โดยการทดสอบนั้น สิ่งต่อไปนี้:

@Test
public void someTest() {
  Bar bar = getBar();

  // do some things

  verify(mockedObject).someMethod(); // <--- will fail
}

สิ่งนี้ล้มเหลวเนื่องจากวัตถุที่เยาะเย้ยได้someMethodเรียกใช้สองครั้ง ฉันไม่ต้องการวิธีการทดสอบของฉันจะดูแลเกี่ยวกับผลข้างเคียงของฉันgetBar()วิธีจึงจะเหมาะสมที่จะตั้งค่าวัตถุจำลองของฉันในตอนท้ายของgetBar()?

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
  reset(mockedObject); // <-- is this OK?
}

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

ทางเลือก

ตัวเลือกทางเลือกน่าจะเรียก:

verify(mockedObject, times(2)).someMethod();

ซึ่งในความคิดของฉันบังคับให้การทดสอบแต่ละครั้งจะรู้เกี่ยวกับความคาดหวังของgetBar()เพื่อประโยชน์

คำตอบ:


60

ฉันเชื่อว่านี่เป็นหนึ่งในกรณีที่การใช้reset()ก็โอเค การทดสอบที่คุณเขียนคือการทดสอบที่ว่า "บางสิ่งบางอย่าง" someMethod()ทริกเกอร์สายเดียวที่จะ การเขียนverify()ข้อความที่มีจำนวนการร้องขอที่แตกต่างกันสามารถทำให้เกิดความสับสนได้

  • atLeastOnce() อนุญาตให้บวกเท็จซึ่งเป็นสิ่งที่ไม่ดีตามที่คุณต้องการให้การทดสอบของคุณถูกต้องเสมอ
  • times(2)ป้องกันการบวกผิด แต่ทำให้ดูเหมือนว่าคุณคาดหวังว่ามีการร้องขอสองครั้งแทนที่จะพูดว่า "ฉันรู้ว่าตัวสร้างเพิ่มขึ้นหนึ่งรายการ" ยิ่งไปกว่านั้นหากมีการเปลี่ยนแปลงบางอย่างใน Constructor เพื่อเพิ่มการเรียกพิเศษตอนนี้การทดสอบมีโอกาสที่จะมีการบวกผิด และการลบการโทรจะทำให้การทดสอบล้มเหลวเนื่องจากการทดสอบผิดตอนนี้แทนที่จะเป็นการทดสอบที่ผิด

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

bar = mock(Bar.class);
//do stuff
verify(bar).someMethod();
reset(bar);
//do other stuff
verify(bar).someMethod2();

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


17
ฉันหวังว่า Mockito จะให้การเรียก resetInteractions () เพื่อลืมการโต้ตอบที่ผ่านมาเพื่อจุดประสงค์ในการตรวจสอบ (... , เวลา (... )) และทำการขัดจังหวะ สิ่งนี้จะทำให้สถานการณ์การทดสอบของ {setup; การกระทำ; ยืนยัน;} ที่จัดการได้ง่ายกว่ามาก มันจะเป็น {setup; resetInteractions; การกระทำ; ยืนยัน}
Arkadiy

2
ที่จริงแล้วตั้งแต่ Mockito 2.1 มันให้วิธีการเคลียร์การร้องขอโดยไม่ต้องรีเซ็ต stubs:Mockito.clearInvocations(T... mocks)
Colin D Bennett

6

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

แทนที่จะreset()โปรดพิจารณาการเขียนง่ายวิธีการทดสอบที่มีขนาดเล็กกว่าและมุ่งเน้นความยาวการทดสอบมากกว่าที่ระบุไว้ กลิ่นรหัสแรกที่อาจเกิดขึ้นอยู่reset()ตรงกลางของวิธีทดสอบ

สกัดจากเอกสาร Mockito

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

หากคุณไม่สนใจเรื่องนั้นจริงๆคุณสามารถใช้:

verify(mockedObject, atLeastOnce()).someMethod();

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


2
ใช่ฉันเคยเห็นคำพูดที่แน่นอน (ฉันเชื่อมโยงกับมันจากคำถามของฉัน) ขณะนี้ฉันยังไม่เห็นข้อโต้แย้งที่ดีว่าทำไมตัวอย่างของฉันด้านบนคือ "ไม่ดี" คุณสามารถจัดหาได้หรือไม่
ดันแคนโจนส์

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

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

มีเหตุผลที่ดีมากมายที่จะใช้การรีเซ็ตฉันจะไม่ให้ความสนใจมากเกินไปในกรณีนี้กับการอ้างอิง mockito คุณสามารถมี JUnit Class Runner ของ Spring เมื่อเรียกใช้ชุดทดสอบทำให้เกิดการโต้ตอบที่ไม่พึงประสงค์โดยเฉพาะอย่างยิ่งถ้าคุณกำลังทำการทดสอบที่เกี่ยวข้องกับการโทรฐานข้อมูลที่เย้ยหยันหรือการโทรที่เกี่ยวข้องกับวิธีการส่วนตัวที่คุณไม่ต้องการใช้
Sandy Simonton

ฉันมักจะพบสิ่งนี้ยากเมื่อฉันต้องการทดสอบหลายสิ่ง แต่ JUnit ไม่ได้เสนอวิธีที่ดี (!) ในการกำหนดค่าการทดสอบ ซึ่งแตกต่างจาก NUnit ทำเช่นกับบันทึกย่อ
Stefan Hendriks

3

ไม่ได้อย่างแน่นอน. บ่อยครั้งที่ความยากลำบากในการเขียนแบบทดสอบสะอาดนั้นเป็นธงสีแดงที่สำคัญเกี่ยวกับการออกแบบรหัสการผลิตของคุณ ในกรณีนี้ทางออกที่ดีที่สุดคือการปรับโครงสร้างรหัสของคุณใหม่เพื่อให้ Constructor ของ Bar ไม่เรียกวิธีการใด ๆ

ตัวสร้างควรสร้างไม่ใช่ใช้ตรรกะ รับค่าตอบแทนของวิธีการและผ่านมันเป็นพารามิเตอร์ตัวสร้าง

new Bar(mockedObject);

กลายเป็น:

new Bar(mockedObject.someMethod());

หากสิ่งนี้ส่งผลให้เกิดการทำซ้ำตรรกะนี้ในหลาย ๆ ที่ให้ลองสร้างวิธีการจากโรงงานซึ่งสามารถทดสอบได้โดยอิสระจากวัตถุบาร์ของคุณ:

public Bar createBar(MockedObject mockedObject) {
    Object dependency = mockedObject.someMethod();
    // ...more logic that used to be in Bar constructor
    return new Bar(dependency);
}

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

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