Mockito Matchers ทำงานอย่างไร?


122

Mockito matchers อาร์กิวเมนต์ (เช่นany, argThat, eq, sameและArgumentCaptor.capture()) มีพฤติกรรมแตกต่างกันมากจาก matchers Hamcrest

  • ตัวจับคู่ Mockito มักทำให้เกิด InvalidUseOfMatchersException แม้ว่าจะอยู่ในโค้ดที่ดำเนินการนานหลังจากใช้ตัวจับคู่ก็ตาม

  • ตัวจับคู่ Mockito มองเห็นกฎแปลก ๆ เช่นกำหนดให้ใช้ Mockito matchers สำหรับอาร์กิวเมนต์ทั้งหมดเท่านั้นหากอาร์กิวเมนต์หนึ่งในวิธีการที่กำหนดใช้ตัวจับคู่

  • Mockito matchers สามารถทำให้เกิด NullPointerException เมื่อแทนที่Answers หรือเมื่อใช้(Integer) any()ฯลฯ

  • การปรับโครงสร้างโค้ดด้วยตัวจับคู่ Mockito ในบางวิธีอาจทำให้เกิดข้อยกเว้นและพฤติกรรมที่ไม่คาดคิดและอาจล้มเหลวทั้งหมด

เหตุใด Mockito matchers จึงได้รับการออกแบบเช่นนี้และมีการใช้งานอย่างไร?

คำตอบ:


236

matchers Mockitoวิธีการแบบคงที่และโทรไปยังวิธีการเหล่านั้นซึ่งยืนอยู่ในข้อโต้แย้งในช่วงสายไปและwhenverify

Hamcrest matchers (เวอร์ชันที่เก็บถาวร) (หรือตัวจับคู่แบบ Hamcrest) เป็นอินสแตนซ์อ็อบเจ็กต์ที่ไม่มีสถานะและมีวัตถุประสงค์ทั่วไปที่ใช้Matcher<T>และแสดงวิธีการmatches(T)ที่ส่งคืนจริงหากอ็อบเจ็กต์ตรงกับเกณฑ์ของ Matcher มีวัตถุประสงค์เพื่อให้ปราศจากผลข้างเคียงและโดยทั่วไปจะใช้ในการยืนยันเช่นด้านล่าง

/* Mockito */  verify(foo).setPowerLevel(gt(9000));
/* Hamcrest */ assertThat(foo.getPowerLevel(), is(greaterThan(9000)));

Mockito matchers มีอยู่แยกต่างหากจากตัวจับคู่แบบ Hamcrest เพื่อให้คำอธิบายของนิพจน์ที่ตรงกันพอดีกับการเรียกใช้เมธอดโดยตรง : Mockito matchers จะส่งคืนTโดยที่วิธีการจับคู่ Hamcrest จะส่งคืนอ็อบเจ็กต์ Matcher (ประเภทMatcher<T>)

matchers Mockito ถูกเรียกด้วยวิธีการแบบคงที่เช่นeq, any, gtและstartsWithบนและorg.mockito.Matchers org.mockito.AdditionalMatchersนอกจากนี้ยังมีอะแดปเตอร์ซึ่งมีการเปลี่ยนแปลงในรุ่น Mockito:

  • สำหรับ Mockito 1.x การMatchersเรียกใช้บางสาย (เช่นintThatหรือargThat) คือ Mockito matchers ที่ยอมรับ Hamcrest matchers เป็นพารามิเตอร์โดยตรง ArgumentMatcher<T>ขยายorg.hamcrest.Matcher<T>ซึ่งใช้ในการเป็นตัวแทน Hamcrest ภายในและเป็นคลาสพื้นฐานของ Hamcrest matcherแทนที่จะเป็นMockito matcher ประเภทใด ๆ
  • สำหรับ Mockito 2.0+ Mockito ไม่มีการพึ่งพาโดยตรงกับ Hamcrest อีกต่อไป Matchersเรียกวลีเป็นintThatหรือargThatห่อArgumentMatcher<T>วัตถุที่ไม่ได้ใช้งานอีกต่อไปorg.hamcrest.Matcher<T>แต่ใช้ในลักษณะที่คล้ายกัน อะแดปเตอร์ Hamcrest เช่นargThatและintThatยังคงมีอยู่ แต่ได้ย้ายไปที่MockitoHamcrestแทน

ไม่ว่าผู้จับคู่จะเป็นแฮมเครสต์หรือสไตล์แฮมเครสต์ก็สามารถปรับเปลี่ยนได้ดังนี้:

/* Mockito matcher intThat adapting Hamcrest-style matcher is(greaterThan(...)) */
verify(foo).setPowerLevel(intThat(is(greaterThan(9000))));

ในข้อความข้างต้น: foo.setPowerLevelเป็นวิธีการที่ยอมรับintไฟล์. is(greaterThan(9000))ส่งคืน a Matcher<Integer>ซึ่งไม่สามารถใช้เป็นsetPowerLevelอาร์กิวเมนต์ได้ ตัวจับคู่ Mockito intThatห่อ Matcher แบบ Hamcrest และส่งคืนintเพื่อให้สามารถปรากฏเป็นอาร์กิวเมนต์ได้ ตัวจับคู่ Mockito ต้องการgt(9000)รวมนิพจน์ทั้งหมดไว้ในการเรียกเพียงครั้งเดียวดังเช่นในบรรทัดแรกของโค้ดตัวอย่าง

ผู้จับคู่ทำอะไร / ส่งคืน

when(foo.quux(3, 5)).thenReturn(true);

เมื่อไม่ใช้ตัวจับคู่อาร์กิวเมนต์ Mockito จะบันทึกค่าอาร์กิวเมนต์ของคุณและเปรียบเทียบกับequalsวิธีการของพวกเขา

when(foo.quux(eq(3), eq(5))).thenReturn(true);    // same as above
when(foo.quux(anyInt(), gt(5))).thenReturn(true); // this one's different

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

matchers กลับค่าหุ่นnullดังกล่าวเป็นศูนย์คอลเลกชันที่ว่างเปล่าหรือ Mockito พยายามที่จะกลับมาเป็นที่ปลอดภัยค่าหุ่นที่เหมาะสมเช่น 0 anyInt()หรือany(Integer.class)หรือที่ว่างเปล่าสำหรับList<String> anyListOf(String.class)เนื่องจากการลบประเภท Mockito จึงขาดข้อมูลประเภทที่จะส่งคืนค่าใด ๆ แต่nullสำหรับany()หรือargThat(...)ซึ่งอาจทำให้เกิด NullPointerException ได้หากพยายาม "unbox อัตโนมัติ" เป็นnullค่าดั้งเดิม

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

ไม่สามารถใช้เมธอด Matcher เป็นค่าส่งคืน ไม่มีทางที่จะใช้วลีthenReturn(anyInt())หรือthenReturn(any(Foo.class))ใน Mockito ได้เช่น Mockito จำเป็นต้องทราบอย่างชัดเจนว่าอินสแตนซ์ใดที่จะส่งคืนในการโทรที่ถูกขีดฆ่าและจะไม่เลือกค่าตอบแทนโดยพลการให้คุณ

รายละเอียดการดำเนินการ

matchers จะถูกเก็บไว้ (เป็น matchers วัตถุ Hamcrest สไตล์) ในสแต็คที่มีอยู่ในระดับที่เรียกว่าArgumentMatcherStorage MockitoCore และ Matchers ต่างเป็นเจ้าของอินสแตนซ์ThreadSafeMockingProgressซึ่งมีอินสแตนซ์ ThreadLocal ที่ถือ MockingProgress แบบคงที่ มันนี้MockingProgressImplที่ถือคอนกรีตArgumentMatcherStorageImpl ดังนั้นสถานะจำลองและการจับคู่จึงเป็นแบบคงที่ แต่มีการกำหนดขอบเขตเธรดอย่างสม่ำเสมอระหว่างคลาส Mockito และ Matchers

ส่วนใหญ่โทรจับคู่เพียง แต่เพิ่มไปยังกองนี้มีข้อยกเว้นสำหรับ matchers เหมือนand,ornotและ สิ่งนี้สอดคล้องอย่างสมบูรณ์กับ (และอาศัย) ลำดับการประเมินของ Javaซึ่งประเมินอาร์กิวเมนต์จากซ้ายไปขวาก่อนที่จะเรียกใช้เมธอด:

when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true);
[6]      [5]  [1]       [4] [2]     [3]

นี่จะ:

  1. เพิ่มanyInt()ลงในสแต็ก
  2. เพิ่มgt(10)ลงในสแต็ก
  3. เพิ่มlt(20)ลงในสแต็ก
  4. ลบgt(10)และและเพิ่มlt(20)and(gt(10), lt(20))
  5. โทรfoo.quux(0, 0)ซึ่ง (ยกเว้นกรณีที่ค้างอยู่เป็นอย่างอื่น) falseผลตอบแทนที่คุ้มค่าเริ่มต้น Mockito ภายในทำเครื่องหมายquux(int, int)ว่าเป็นการโทรล่าสุด
  6. การโทรwhen(false)ซึ่งจะละทิ้งอาร์กิวเมนต์และเตรียมที่จะใช้วิธีการต้นขั้วที่quux(int, int)ระบุใน 5 สถานะที่ถูกต้องมีเพียงสองสถานะเท่านั้นที่มีความยาวสแต็ก 0 (ความเท่าเทียมกัน) หรือ 2 (ตัวจับคู่) และมีตัวจับคู่สองตัวบนสแต็ก (ขั้นตอนที่ 1 และ 4) ดังนั้น Mockito จะตัดเมธอดด้วยตัวany()จับคู่สำหรับอาร์กิวเมนต์แรกและand(gt(10), lt(20))สำหรับอาร์กิวเมนต์ที่สองและล้างสแต็ก

นี่แสดงให้เห็นกฎบางประการ:

  • Mockito ไม่สามารถบอกความแตกต่างระหว่างและquux(anyInt(), 0) quux(0, anyInt())ทั้งคู่ดูเหมือนโทรquux(0, 0)คุยกับ int matcher หนึ่งคนในสแต็ก ดังนั้นหากคุณใช้ตัวจับคู่หนึ่งตัวคุณจะต้องจับคู่อาร์กิวเมนต์ทั้งหมด

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

    int between10And20 = and(gt(10), lt(20));
    /* BAD */ when(foo.quux(anyInt(), between10And20)).thenReturn(true);
    // Mockito sees the stack as the opposite: and(gt(10), lt(20)), anyInt().
    
    public static int anyIntBetween10And20() { return and(gt(10), lt(20)); }
    /* OK */  when(foo.quux(anyInt(), anyIntBetween10And20())).thenReturn(true);
    // The helper method calls the matcher methods in the right order.
  • กองซ้อนเปลี่ยนแปลงบ่อยพอที่ Mockito ไม่สามารถตำรวจได้อย่างระมัดระวัง สามารถตรวจสอบสแต็กได้เฉพาะเมื่อคุณโต้ตอบกับ Mockito หรือล้อเลียนและต้องยอมรับตัวจับคู่โดยไม่ทราบว่าถูกใช้ทันทีหรือละทิ้งโดยไม่ตั้งใจ ตามทฤษฎีแล้วสแต็กควรว่างเปล่าเสมอนอกการเรียกwhenหรือverifyแต่ Mockito ไม่สามารถตรวจสอบได้โดยอัตโนมัติ Mockito.validateMockitoUsage()คุณสามารถตรวจสอบด้วยตนเอง

  • ในการโทรหาwhenMockito จะเรียกเมธอดที่เป็นปัญหาซึ่งจะทำให้เกิดข้อยกเว้นหากคุณได้ขีดฆ่าวิธีการที่จะโยนข้อยกเว้น (หรือต้องการค่าที่ไม่ใช่ศูนย์หรือไม่ใช่ค่าว่าง) doReturnและdoAnswer(ฯลฯ ) ไม่เรียกใช้วิธีการจริงและมักเป็นทางเลือกที่มีประโยชน์

  • หากคุณเรียกวิธีการจำลองที่อยู่ตรงกลางของการeqขีดทับ(เช่นการคำนวณคำตอบสำหรับตัวจับคู่) Mockito จะตรวจสอบความยาวสแต็กเทียบกับการเรียกนั้นแทนและอาจล้มเหลว

  • หากคุณพยายามทำบางสิ่งที่ไม่ดีเช่นการขีดฆ่า/ การตรวจสอบวิธีสุดท้าย Mockito จะเรียกวิธีการจริงและทิ้งตัวจับคู่พิเศษไว้ในสแต็finalเรียกวิธีการอาจจะไม่โยนยกเว้น แต่คุณอาจจะได้รับการInvalidUseOfMatchersExceptionจาก matchers จรจัดเมื่อคุณมีปฏิสัมพันธ์ต่อไปด้วยจำลอง

ปัญหาทั่วไป

  • InvalidUseOfMatchersException :

    • ตรวจสอบว่าทุกอาร์กิวเมนต์มีการเรียกจับคู่เพียงครั้งเดียวถ้าคุณใช้ตัวจับคู่เลยและคุณไม่ได้ใช้ตัวจับคู่นอกการเรียกwhenหรือ verifyไม่ควรใช้ Matchers เป็นค่าที่ส่งคืนหรือฟิลด์ / ตัวแปร

    • ตรวจสอบว่าคุณไม่ได้เรียกการล้อเลียนซึ่งเป็นส่วนหนึ่งของการให้อาร์กิวเมนต์ที่ตรงกัน

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

  • NullPointerException พร้อมอาร์กิวเมนต์ดั้งเดิม: (Integer) any()คืนค่า null ในขณะที่any(Integer.class)ส่งกลับ 0; สิ่งนี้อาจทำให้เกิดNullPointerExceptionถ้าคุณคาดหวังว่าจะได้รับintแทนที่จะเป็นจำนวนเต็ม ไม่ว่าในกรณีใดก็ตามชอบanyInt()ซึ่งจะคืนค่าศูนย์และข้ามขั้นตอนการชกมวยอัตโนมัติ

  • NullPointerException หรือข้อยกเว้นอื่น ๆ : Call to when(foo.bar(any())).thenReturn(baz)จะเรียก จริงfoo.bar(null)ซึ่งคุณอาจถูกขีดฆ่าเพื่อโยนข้อยกเว้นเมื่อได้รับอาร์กิวเมนต์ว่าง สลับไปข้ามพฤติกรรมdoReturn(baz).when(foo).bar(any()) stubbed

การแก้ไขปัญหาทั่วไป

  • ใช้MockitoJUnitRunnerหรือเรียกอย่างชัดเจนvalidateMockitoUsageในวิธีtearDownหรือของคุณ@After(ซึ่งนักวิ่งจะทำเพื่อคุณโดยอัตโนมัติ) วิธีนี้จะช่วยตรวจสอบว่าคุณใช้ตัวจับคู่ผิดหรือไม่

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


2
ขอบคุณสำหรับบทความนี้ NullPointerException ที่มีรูปแบบ when / thenReturn ทำให้ฉันมีปัญหาจนกระทั่งฉันเปลี่ยนเป็น doReturn / when
yngwietiger

11

เพียงเล็กน้อยสำหรับคำตอบที่ยอดเยี่ยมของ Jeff Bowman เนื่องจากฉันพบคำถามนี้เมื่อค้นหาวิธีแก้ปัญหาของตัวเอง:

หากการโทรไปยังเมธอดตรงกับwhenการโทรที่ได้รับการฝึกฝนมาแล้วมากกว่าหนึ่งครั้งลำดับของการwhenโทรนั้นมีความสำคัญและควรมีขนาดกว้างที่สุดไปจนถึงเฉพาะเจาะจงมากที่สุด เริ่มจากหนึ่งในตัวอย่างของ Jeff:

when(foo.quux(anyInt(), anyInt())).thenReturn(true);
when(foo.quux(anyInt(), eq(5))).thenReturn(false);

เป็นคำสั่งที่ทำให้แน่ใจว่า (อาจ) ได้ผลลัพธ์ที่ต้องการ:

foo.quux(3 /*any int*/, 8 /*any other int than 5*/) //returns true
foo.quux(2 /*any int*/, 5) //returns false

หากคุณผกผันเวลาที่โทรผลลัพธ์จะเป็นเสมอ trueหากคุณผกผันเมื่อสายแล้วผลจะเป็นเสมอ


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

1
@JeffBowman ฉันคิดว่ามันสมเหตุสมผลสำหรับคำถามนี้เนื่องจากคำถามเกี่ยวกับตัวจับคู่แบบจำลองและตัวจับคู่สามารถนำมาใช้เมื่อทำให้ขาด (เช่นเดียวกับในตัวอย่างส่วนใหญ่ของคุณ) เนื่องจากการค้นหาคำอธิบายของ Google ทำให้ฉันได้รับคำถามนี้ฉันคิดว่าการมีข้อมูลนี้ที่นี่มีประโยชน์
tibtof
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.