คู่มือผู้ปฏิบัติงานทดสอบหน่วยสำหรับ Mockito ทุกวัน

เผยแพร่แล้ว: 2022-03-11

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

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

ได้รับ Mockito

การรับ Mockito เป็นเรื่องง่ายในทุกวันนี้ หากคุณกำลังใช้ Gradle การเพิ่มบรรทัดเดียวนี้ลงในสคริปต์บิลด์ของคุณ:

 testCompile "org.mockito:mockito−core:2.7.7"

สำหรับผู้ที่ยังชอบ Maven เพียงเพิ่ม Mockito ในการพึ่งพาของคุณดังนี้:

 <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.7</version> <scope>test</scope> </dependency>

แน่นอน โลกกว้างกว่า Maven และ Gradle มาก คุณสามารถใช้เครื่องมือการจัดการโปรเจ็กต์ใดก็ได้เพื่อดึงสิ่งประดิษฐ์ขวดโหล Mockito จากที่เก็บส่วนกลางของ Maven

ใกล้ม็อกคิโต

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

เราจะมุ่งเน้นไปที่การทดสอบสองประเภทเท่านั้น 'เยาะเย้ย' และ 'สายลับ' เนื่องจากม็อคคิโตใช้สิ่งเหล่านี้อย่างหนัก

เยาะเย้ย

การเยาะเย้ยคืออะไร? แน่นอนว่าไม่ใช่จุดที่คุณจะล้อเลียนนักพัฒนาซอฟต์แวร์คนอื่นๆ การเยาะเย้ยสำหรับการทดสอบหน่วยคือเมื่อคุณสร้างวัตถุที่นำพฤติกรรมของระบบย่อยจริงไปใช้ในลักษณะที่มีการควบคุม กล่าวโดยย่อ mocks ใช้แทนการพึ่งพา

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

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

สายลับ

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

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

การสร้างตัวอย่างง่ายๆ

มาดูการสาธิตง่ายๆ ที่เราสามารถเขียนการทดสอบได้ สมมติว่าเรามีอินเทอร์เฟซ UserRepository ด้วยวิธีเดียวในการค้นหาผู้ใช้โดยใช้ตัวระบุ เรายังมีแนวคิดของตัวเข้ารหัสรหัสผ่านเพื่อเปลี่ยนรหัสผ่านแบบข้อความธรรมดาให้เป็นแฮชรหัสผ่าน ทั้ง UserRepository และ PasswordEncoder เป็นการพึ่งพา (เรียกอีกอย่างว่าผู้ทำงานร่วมกัน) ของ UserService ฉีดผ่านตัวสร้าง โค้ดสาธิตของเรามีลักษณะดังนี้:

UserRepository
 public interface UserRepository { User findById(String id); }
ผู้ใช้
 public class User { private String id; private String passwordHash; private boolean enabled; public User(String id, String passwordHash, boolean enabled) { this.id = id; this.passwordHash = passwordHash; this.enabled = enabled; } ... }
ตัวเข้ารหัสรหัสผ่าน
 public interface PasswordEncoder { String encode(String password); }
บริการผู้ใช้
 public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public boolean isValidUser(String id, String password) { User user = userRepository.findById(id); return isEnabledUser(user) && isValidPassword(user, password); } private boolean isEnabledUser(User user) { return user != null && user.isEnabled(); } private boolean isValidPassword(User user, String password) { String encodedPassword = passwordEncoder.encode(password); return encodedPassword.equals(user.getPasswordHash()); } }

โค้ดตัวอย่างนี้มีอยู่ใน GitHub ดังนั้นคุณจึงสามารถดาวน์โหลดโค้ดตัวอย่างนี้เพื่อตรวจสอบควบคู่ไปกับบทความนี้

สมัคร Mockito

โดยใช้โค้ดตัวอย่างของเรา มาดูวิธีการใช้ Mockito และเขียนการทดสอบกัน

การสร้างหุ่นจำลอง

ด้วย Mockito การสร้างแบบจำลองนั้นง่ายพอ ๆ กับการเรียกวิธีการแบบคงที่ Mockito.mock() :

 import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);

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

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

โปรดทราบด้วยว่าวิธี equals() และ hashCode() ไม่สามารถเยาะเย้ยได้

การสร้างสายลับ

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

 DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals("42", decimalFormat.format(42L));

การสร้างสายลับไม่ได้แตกต่างจากการสร้างแบบจำลองมากนัก นอกจากนี้ วิธี Mockito ทั้งหมดที่ใช้สำหรับกำหนดค่าการจำลองยังใช้ได้กับการกำหนดค่าสายลับด้วย

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

ค่าส่งคืนเริ่มต้น

การเรียก mock(PasswordEncoder.class) ส่งคืนอินสแตนซ์ของ PasswordEncoder เราสามารถเรียกวิธีการของมันได้ แต่สิ่งที่พวกเขาจะกลับมา? โดยค่าเริ่มต้น วิธีการทั้งหมดของการจำลองจะส่งกลับค่า "ที่ไม่ได้กำหนดค่าเริ่มต้น" หรือ "ว่าง" เช่น ศูนย์สำหรับประเภทตัวเลข (ทั้งแบบดั้งเดิมและแบบกล่อง) เท็จสำหรับบูลีน และค่าว่างสำหรับประเภทอื่นๆ ส่วนใหญ่

พิจารณาอินเทอร์เฟซต่อไปนี้:

 interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection<String> getCollection(); String[] getArray(); Stream<?> getStream(); Optional<?> getOptional(); }

ตอนนี้ให้พิจารณาตัวอย่างต่อไปนี้ ซึ่งให้แนวคิดเกี่ยวกับค่าเริ่มต้นที่คาดหวังจากวิธีการจำลอง:

 Demo demo = mock(Demo.class); assertEquals(0, demo.getInt()); assertEquals(0, demo.getInteger().intValue()); assertEquals(0d, demo.getDouble(), 0d); assertFalse(demo.getBoolean()); assertNull(demo.getObject()); assertEquals(Collections.emptyList(), demo.getCollection()); assertNull(demo.getArray()); assertEquals(0L, demo.getStream().count()); assertFalse(demo.getOptional().isPresent());

วิธีการขัดถู

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

Mockito เสนอวิธีการขัดสองวิธี วิธีแรกคือ “ เมื่อ เรียกวิธีนี้ แล้วค่อย ทำบางอย่าง” พิจารณาตัวอย่างต่อไปนี้:

 when(passwordEncoder.encode("1")).thenReturn("a");

อ่านเกือบจะเหมือนภาษาอังกฤษ: “เมื่อเรียก passwordEncoder.encode(“1”) ให้ส่งคืน a

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

 doReturn("a").when(passwordEncoder).encode("1");

ข้อมูลโค้ดที่มีวิธีการ stubbing นี้จะอ่านว่า: “Return a เมื่อเมธอด encode() ของ passwordEncoder ถูกเรียกด้วยอาร์กิวเมนต์ 1

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

มาสำรวจวิธีการขัดถูโดย Mockito กัน เราจะรวมทั้งสองวิธีในการขัดจังหวะในตัวอย่างของเรา

คืนค่า

thenReturn หรือ doReturn() ใช้เพื่อระบุค่าที่จะส่งคืนเมื่อมีการเรียกใช้เมธอด

 //”when this method is called, then do something” when(passwordEncoder.encode("1")).thenReturn("a");

หรือ

 //”do something when this mock's method is called with the following arguments” doReturn("a").when(passwordEncoder).encode("1");

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

 //when when(passwordEncoder.encode("1")).thenReturn("a", "b");

หรือ

 //do doReturn("a", "b").when(passwordEncoder).encode("1");

สามารถทำได้เช่นเดียวกันกับตัวอย่างต่อไปนี้:

 when(passwordEncoder.encode("1")) .thenReturn("a") .thenReturn("b");

รูปแบบนี้ยังสามารถใช้กับวิธีการขัดจังหวะอื่นๆ เพื่อกำหนดผลลัพธ์ของการเรียกต่อเนื่องกัน

ส่งคืนการตอบกลับที่กำหนดเอง

then() นามแฝงของ thenAnswer() และ doAnswer() บรรลุสิ่งเดียวกัน ซึ่งตั้งค่าคำตอบที่กำหนดเองให้ส่งคืนเมื่อมีการเรียกเมธอด เช่น:

 when(passwordEncoder.encode("1")).thenAnswer( invocation -> invocation.getArgument(0) + "!");

หรือ

 doAnswer(invocation -> invocation.getArgument(0) + "!") .when(passwordEncoder).encode("1");

อาร์กิวเมนต์เดียวที่ thenAnswer() รับคือการใช้งานอินเทอร์เฟซ Answer มีเมธอดเดียวที่มีพารามิเตอร์ประเภท InvocationOnMock

คุณยังสามารถโยนข้อยกเว้นอันเป็นผลมาจากการเรียกใช้เมธอด:

 when(passwordEncoder.encode("1")).thenAnswer(invocation -> { throw new IllegalArgumentException(); });

…หรือเรียกเมธอดที่แท้จริงของคลาส (ใช้ไม่ได้กับอินเตอร์เฟส):

 Date mock = mock(Date.class); doAnswer(InvocationOnMock::callRealMethod).when(mock).setTime(42); doAnswer(InvocationOnMock::callRealMethod).when(mock).getTime(); mock.setTime(42); assertEquals(42, mock.getTime());

คุณคิดถูกแล้วที่คิดว่ามันดูยุ่งยาก Mockito จัดเตรียม thenCallRealMethod() และ thenThrow() เพื่อปรับปรุงการทดสอบของคุณในด้านนี้

เรียกวิธีการจริง

ตามชื่อของมันแล้ว thenCallRealMethod() และ doCallRealMethod() เรียกวิธีการจริงบนวัตถุจำลอง:

 Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());

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

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

 when(passwordEncoder.encode("1")).thenCallRealMethod();

Mockito จะล้มเหลวด้วยข้อความต่อไปนี้:

 Cannot call abstract real method on java object! Calling real methods is only possible when mocking non abstract method. //correct example: when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();

ขอชื่นชมนักพัฒนา Mockito ที่เอาใจใส่มากพอที่จะให้คำอธิบายอย่างละเอียด!

โยนข้อยกเว้น

thenThrow() และ doThrow() กำหนดค่าวิธีการเยาะเย้ยเพื่อให้เกิดข้อยกเว้น:

 when(passwordEncoder.encode("1")).thenThrow(new IllegalArgumentException());

หรือ

 doThrow(new IllegalArgumentException()).when(passwordEncoder).encode("1");

Mockito ช่วยให้แน่ใจว่าข้อยกเว้นที่ส่งออกมานั้นถูกต้องสำหรับวิธีการ stubbed นั้นและจะบ่นหากข้อยกเว้นไม่อยู่ในรายการข้อยกเว้นที่ตรวจสอบของวิธีการ พิจารณาสิ่งต่อไปนี้:

 when(passwordEncoder.encode("1")).thenThrow(new IOException());

จะทำให้เกิดข้อผิดพลาด:

 org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException

อย่างที่คุณเห็น Mockito ตรวจพบว่า encode() ไม่สามารถส่ง IOException ได้

คุณยังสามารถส่งคลาสของข้อยกเว้นแทนที่จะส่งอินสแตนซ์ของข้อยกเว้น:

 when(passwordEncoder.encode("1")).thenThrow(IllegalArgumentException.class);

หรือ

 doThrow(IllegalArgumentException.class).when(passwordEncoder).encode("1");

ที่กล่าวว่า Mockito ไม่สามารถตรวจสอบคลาสข้อยกเว้นในลักษณะเดียวกับที่จะตรวจสอบอินสแตนซ์ข้อยกเว้น ดังนั้นคุณต้องมีวินัยและไม่ผ่านวัตถุคลาสที่ผิดกฎหมาย ตัวอย่างเช่น สิ่งต่อไปนี้จะส่ง IOException แม้ว่า encode() จะไม่ส่งข้อยกเว้นที่ตรวจสอบ:

 when(passwordEncoder.encode("1")).thenThrow(IOException.class); passwordEncoder.encode("1");

การเยาะเย้ยอินเทอร์เฟซด้วยวิธีการเริ่มต้น

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

พิจารณาตัวอย่างต่อไปนี้:

 interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());

ในตัวอย่างนี้ assertFalse() จะสำเร็จ หากนั่นไม่ใช่สิ่งที่คุณคาดไว้ ตรวจสอบให้แน่ใจว่าคุณได้ใช้ Mockito เรียกวิธีการที่แท้จริง เช่น:

 AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());

การจับคู่อาร์กิวเมนต์

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

แม้ว่าบางครั้ง เราไม่ทราบค่าเหล่านี้ล่วงหน้า

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

พิจารณาตัวอย่างต่อไปนี้:

 when(passwordEncoder.encode(anyString())).thenReturn("exact"); assertEquals("exact", passwordEncoder.encode("1")); assertEquals("exact", passwordEncoder.encode("abc"));

คุณจะเห็นได้ว่าผลลัพธ์จะเหมือนกันไม่ว่าค่าใดที่เราส่งไปยัง encode() เนื่องจากเราใช้ตัวจับคู่อาร์กิวเมนต์ anyString() ในบรรทัดแรกนั้น หากเราเขียนบรรทัดนั้นใหม่เป็นภาษาอังกฤษธรรมดา จะฟังดูเหมือน “เมื่อมีการขอให้ตัวเข้ารหัสรหัสผ่านเข้ารหัสสตริงใดๆ แล้วส่งคืนสตริง 'ที่แน่นอน'”

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

 abstract class AClass { public abstract boolean call(String s, int i); } AClass mock = mock(AClass.class); //This doesn't work. when(mock.call("a", anyInt())).thenReturn(true);

ในการแก้ไขข้อผิดพลาด เราต้องแทนที่บรรทัดสุดท้ายเพื่อรวมตัวจับคู่อาร์กิวเมนต์ eq สำหรับ a ดังต่อไปนี้:

 when(mock.call(eq("a"), anyInt())).thenReturn(true);

ที่นี่เราได้ใช้ตัวจับคู่อาร์กิวเมนต์ eq() และ anyInt() แต่มีอย่างอื่นอีกมากมาย สำหรับรายการอาร์กิวเมนต์ที่ตรงกันทั้งหมด โปรดดูเอกสารในคลาส org.mockito.ArgumentMatchers

สิ่งสำคัญคือต้องทราบว่าคุณไม่สามารถใช้ตัวจับคู่อาร์กิวเมนต์นอกการตรวจสอบหรือการตัดทอน ตัวอย่างเช่น คุณไม่สามารถมีสิ่งต่อไปนี้:

 //this won't work String orMatcher = or(eq("a"), endsWith("b")); verify(mock).encode(orMatcher);

Mockito จะตรวจจับการจับคู่อาร์กิวเมนต์ที่วางผิดตำแหน่งและโยน InvalidUseOfMatchersException การตรวจสอบด้วยการจับคู่อาร์กิวเมนต์ควรทำดังนี้:

 verify(mock).encode(or(eq("a"), endsWith("b")));

ตัวจับคู่อาร์กิวเมนต์ไม่สามารถใช้เป็นค่าส่งคืนได้เช่นกัน Mockito ไม่สามารถส่งคืน anyString() หรืออะไรก็ได้ ต้องใช้ค่าที่แน่นอนเมื่อมีการโทร

การจับคู่แบบกำหนดเอง

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

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

พิจารณาตัวอย่างต่อไปนี้:

 FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher<File> hasLuck = file -> file.getName().endsWith("luck"); when(fileFilter.accept(argThat(hasLuck))).thenReturn(true); assertFalse(fileFilter.accept(new File("/deserve"))); assertTrue(fileFilter.accept(new File("/deserve/luck")));

ที่นี่เราสร้างตัวจับคู่อาร์กิวเมนต์ hasLuck และใช้ argThat() เพื่อส่งตัวจับคู่เป็นอาร์กิวเมนต์ไปยังวิธีการเยาะเย้ย โดยให้ค่า true หากชื่อไฟล์ลงท้ายด้วย "โชค" คุณสามารถถือว่า ArgumentMatcher เป็นอินเทอร์เฟซที่ใช้งานได้ และสร้างอินสแตนซ์ด้วยแลมบ์ดา (ซึ่งเป็นสิ่งที่เราทำในตัวอย่าง) ไวยากรณ์ที่กระชับน้อยกว่าจะมีลักษณะดังนี้:

 ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };

หากคุณต้องการสร้างตัวจับคู่อาร์กิวเมนต์ที่ใช้งานได้กับประเภทดั้งเดิม มีวิธีการอื่นอีกหลายวิธีใน org.mockito.ArgumentMatchers :

  • charThat(ตัวจับคู่ ArgumentMatcher<Character>)
  • booleanThat(ตัวจับคู่อาร์กิวเมนต์<บูลีน>)
  • byteThat (ตัวจับคู่ ArgumentMatcher<Byte>)
  • shortThat(ตัวจับคู่ ArgumentMatcher<Short>)
  • intThat(ตัวจับคู่ ArgumentMatcher<Integer>)
  • longThat(ตัวจับคู่อาร์กิวเมนต์<Long>)
  • floatThat(ตัวจับคู่ ArgumentMatcher<Float>)
  • doubleThat(ตัวจับคู่อาร์กิวเมนต์<คู่>)

การรวมตัวจับคู่

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

พิจารณาตัวอย่างต่อไปนี้:

 when(passwordEncoder.encode(or(eq("1"), contains("a")))).thenReturn("ok"); assertEquals("ok", passwordEncoder.encode("1")); assertEquals("ok", passwordEncoder.encode("123abc")); assertNull(passwordEncoder.encode("123"));

เราได้รวมผลลัพธ์ของการจับคู่อาร์กิวเมนต์สองตัว: eq("1") และ contain contains("a") นิพจน์สุดท้าย or(eq("1"), contains("a")) อาจถูกตีความว่าเป็น "สตริงอาร์กิวเมนต์ต้อง เท่ากับ "1" หรือ มี "a"

โปรดทราบว่ามีตัวจับคู่ทั่วไปน้อยกว่าที่ระบุไว้ในคลาส org.mockito.AdditionalMatchers เช่น geq() , leq() , gt() และ lt() ซึ่งเป็นการเปรียบเทียบค่าที่ใช้ได้สำหรับค่าดั้งเดิมและอินสแตนซ์ของ java.lang.Comparable ได้.

การตรวจสอบพฤติกรรม

เมื่อมีการใช้ล้อเลียนหรือสายลับแล้ว เราสามารถ verify ได้ว่ามีการโต้ตอบเฉพาะเกิดขึ้น แท้จริงแล้วเรากำลังพูดว่า "เฮ้ Mockito ตรวจสอบให้แน่ใจว่าวิธีนี้ถูกเรียกด้วยข้อโต้แย้งเหล่านี้"

พิจารณาตัวอย่างเทียมต่อไปนี้:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode("a")).thenReturn("1"); passwordEncoder.encode("a"); verify(passwordEncoder).encode("a");

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

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

ตัวจับคู่อาร์กิวเมนต์สามารถใช้สำหรับการตรวจสอบได้เช่นเดียวกับการมัด:

 verify(passwordEncoder).encode(anyString());

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

 // verify the exact number of invocations verify(passwordEncoder, times(42)).encode(anyString()); // verify that there was at least one invocation verify(passwordEncoder, atLeastOnce()).encode(anyString()); // verify that there were at least five invocations verify(passwordEncoder, atLeast(5)).encode(anyString()); // verify the maximum number of invocations verify(passwordEncoder, atMost(5)).encode(anyString()); // verify that it was the only invocation and // that there're no more unverified interactions verify(passwordEncoder, only()).encode(anyString()); // verify that there were no invocations verify(passwordEncoder, never()).encode(anyString());

ฟีเจอร์ที่ไม่ค่อยได้ใช้ของ verify() คือความสามารถในการล้มเหลวในการหมดเวลา ซึ่งมีประโยชน์อย่างมากสำหรับการทดสอบโค้ดพร้อมกัน ตัวอย่างเช่น หากมีการเรียกตัวเข้ารหัสรหัสผ่านของเราในเธรดอื่นพร้อมกับ verify() เราสามารถเขียนการทดสอบได้ดังนี้:

 usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode("a");

การทดสอบนี้จะสำเร็จหากเรียกใช้ encode() และเสร็จสิ้นภายใน 500 มิลลิวินาทีหรือน้อยกว่า หากคุณต้องการรอจนครบระยะเวลาที่คุณระบุ ให้ใช้ after() แทน timeout() :

 verify(passwordEncoder, after(500)).encode("a");

โหมดการยืนยันอื่นๆ ( times() , atLeast() ฯลฯ) สามารถใช้ร่วมกับ timeout() และ after() เพื่อทำการทดสอบที่ซับซ้อนยิ่งขึ้น:

 // passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode("a");

นอกจาก times() แล้ว โหมดการยืนยันที่รองรับ ได้แก่ only() , atLeast() และ atLeastOnce() (เป็นนามแฝงของ atLeast(1) )

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

 PasswordEncoder first = mock(PasswordEncoder.class); PasswordEncoder second = mock(PasswordEncoder.class); // simulate calls first.encode("f1"); second.encode("s1"); first.encode("f2"); // verify call order InOrder inOrder = inOrder(first, second); inOrder.verify(first).encode("f1"); inOrder.verify(second).encode("s1"); inOrder.verify(first).encode("f2");

หากเราจัดลำดับการเรียกที่จำลองใหม่ การทดสอบจะล้มเหลวด้วย VerificationInOrderFailure

สามารถตรวจสอบการไม่มีการร้องขอได้โดยใช้ verifyZeroInteractions() เมธอดนี้ยอมรับการเยาะเย้ยหรือการเยาะเย้ยเป็นอาร์กิวเมนต์ และจะล้มเหลวหากมีการเรียกวิธีการใด ๆ ของการจำลองที่ส่งผ่าน

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

การจับข้อโต้แย้ง

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

มาสร้างแบบจำลองของ PasswordEncoder กัน เรียก encode() จับอาร์กิวเมนต์ และตรวจสอบค่าของมัน:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("password", passwordCaptor.getValue());

อย่างที่คุณเห็น เราส่ง passwordCaptor.capture() เป็นอาร์กิวเมนต์ของ encode() สำหรับการตรวจสอบ สิ่งนี้สร้างตัวจับคู่อาร์กิวเมนต์ภายในที่บันทึกอาร์กิวเมนต์ จากนั้นเราดึงค่าที่จับได้ด้วย passwordCaptor.getValue() และตรวจสอบด้วย assertEquals()

หากเราต้องการจับอาร์กิวเมนต์ในการเรียกหลาย ๆ ครั้ง ArgumentCaptor ช่วยให้คุณสามารถดึงค่าทั้งหมดด้วย getAllValues() เช่น:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password1"); passwordEncoder.encode("password2"); passwordEncoder.encode("password3"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList("password1", "password2", "password3"), passwordCaptor.getAllValues());

เทคนิคเดียวกันนี้สามารถใช้สำหรับจับอาร์กิวเมนต์ของเมธอดตัวแปร arity (หรือที่เรียกว่า varargs)

การทดสอบตัวอย่างง่ายๆของเรา

ตอนนี้เรารู้มากขึ้นเกี่ยวกับ Mockito แล้ว ก็ถึงเวลากลับไปที่ตัวอย่างของเรา มาเขียนการทดสอบเมธอด isValidUser กัน นี่คือสิ่งที่อาจดูเหมือน:

 public class UserServiceTest { private static final String PASSWORD = "password"; private static final User ENABLED_USER = new User("user id", "hash", true); private static final User DISABLED_USER = new User("disabled user id", "disabled user password hash", false); private UserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; @Before public void setup() { userRepository = createUserRepository(); passwordEncoder = createPasswordEncoder(); userService = new UserService(userRepository, passwordEncoder); } @Test public void shouldBeValidForValidCredentials() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), PASSWORD); assertTrue(userIsValid); // userRepository had to be used to find a user with verify(userRepository).findById(ENABLED_USER.getId()); // passwordEncoder had to be used to compute a hash of "password" verify(passwordEncoder).encode(PASSWORD); } @Test public void shouldBeInvalidForInvalidId() { boolean userIsValid = userService.isValidUser("invalid id", PASSWORD); assertFalse(userIsValid); InOrder inOrder = inOrder(userRepository, passwordEncoder); inOrder.verify(userRepository).findById("invalid id"); inOrder.verify(passwordEncoder, never()).encode(anyString()); } @Test public void shouldBeInvalidForInvalidPassword() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), "invalid"); assertFalse(userIsValid); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("invalid", passwordCaptor.getValue()); } @Test public void shouldBeInvalidForDisabledUser() { boolean userIsValid = userService.isValidUser(DISABLED_USER.getId(), PASSWORD); assertFalse(userIsValid); verify(userRepository).findById(DISABLED_USER.getId()); verifyZeroInteractions(passwordEncoder); } private PasswordEncoder createPasswordEncoder() { PasswordEncoder mock = mock(PasswordEncoder.class); when(mock.encode(anyString())).thenReturn("any password hash"); when(mock.encode(PASSWORD)).thenReturn(ENABLED_USER.getPasswordHash()); return mock; } private UserRepository createUserRepository() { UserRepository mock = mock(UserRepository.class); when(mock.findById(ENABLED_USER.getId())).thenReturn(ENABLED_USER); when(mock.findById(DISABLED_USER.getId())).thenReturn(DISABLED_USER); return mock; } }

ดำน้ำใต้ API

Mockito มี API ที่อ่านสะดวกและสะดวก แต่มาสำรวจการทำงานภายในกันบ้างเพื่อทำความเข้าใจข้อจำกัดและหลีกเลี่ยงข้อผิดพลาดแปลก ๆ

มาตรวจสอบสิ่งที่เกิดขึ้นภายใน Mockito เมื่อมีการเรียกใช้ตัวอย่างต่อไปนี้:

 // 1: create PasswordEncoder mock = mock(PasswordEncoder.class); // 2: stub when(mock.encode("a")).thenReturn("1"); // 3: act mock.encode("a"); // 4: verify verify(mock).encode(or(eq("a"), endsWith("b")));

เห็นได้ชัดว่าบรรทัดแรกสร้างการเยาะเย้ย Mockito ใช้ ByteBuddy เพื่อสร้างคลาสย่อยของคลาสที่กำหนด ออบเจ็กต์คลาสใหม่มีชื่อที่สร้างขึ้นเช่น demo.mockito.PasswordEncoder$MockitoMock$1953422997 ซึ่ง equals() จะทำหน้าที่เป็นการตรวจสอบตัวตนและ hashCode() จะส่งคืนรหัสแฮชเอกลักษณ์ เมื่อสร้างและโหลดคลาสแล้ว อินสแตนซ์ของคลาสจะถูกสร้างขึ้นโดยใช้ Objenesis

ลองดูบรรทัดถัดไป:

 when(mock.encode("a")).thenReturn("1");

ลำดับมีความสำคัญ: คำสั่งแรกที่ดำเนินการที่นี่คือ mock.encode("a") ซึ่งจะเรียกใช้ encode() บนการจำลองด้วยค่าส่งคืนเริ่มต้นเป็น null จริงๆ แล้ว เรากำลังส่ง null เป็นอาร์กิวเมนต์ของ when() Mockito ไม่สนใจว่าค่าที่แน่นอนจะถูกส่งต่อไปยัง when() เพราะมันเก็บข้อมูลเกี่ยวกับการเรียกของวิธีการเยาะเย้ยในสิ่งที่เรียกว่า 'การขัดจังหวะต่อเนื่อง' เมื่อมีการเรียกใช้เมธอดนั้น ต่อมา เมื่อเราเรียก when() Mockito จะดึงออบเจกต์ stubbing ที่ต่อเนื่องและส่งคืนเป็นผลลัพธ์ของ when() จากนั้นเราเรียก thenReturn(“1”) กับวัตถุ stubbing ต่อเนื่องที่ส่งคืน

บรรทัดที่สาม mock.encode("a"); ง่ายมาก: เรากำลังเรียกวิธีการที่มีการตัดทอน ภายใน Mockito จะบันทึกการเรียกนี้สำหรับการตรวจสอบเพิ่มเติมและส่งคืนคำตอบสำหรับการร้องขอที่คลุมเครือ ในกรณีของเราคือ string 1

ในบรรทัดที่สี่ ( verify(mock).encode(or(eq("a"), endsWith("b"))); ) เราขอให้ Mockito ตรวจสอบว่ามีการเรียกใช้ encode() กับสิ่งเหล่านั้น อาร์กิวเมนต์เฉพาะ

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

Mockito สร้าง or จับคู่อย่างไร? ขั้นแรก eq("a") ถูกเรียก และเพิ่มตัวจับคู่ที่ equals ลงในสแต็กตัวจับคู่ อย่างที่สอง endsWith("b") และเพิ่มตัวจับคู่ endsWith ลงในสแต็ก ในที่สุด or(null, null) ถูกเรียก—มันใช้ตัวจับคู่สองตัวที่ปรากฏขึ้นจากสแต็ก สร้างตัวจับคู่ or และผลักสิ่งนั้นไปยังสแต็ก ในที่สุด encode() ถูกเรียก จากนั้น Mockito จะตรวจสอบว่ามีการเรียกใช้เมธอดตามจำนวนที่คาดไว้และด้วยอาร์กิวเมนต์ที่คาดไว้

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

 verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq("a"), endsWith("b")); }

การเปลี่ยนคำตอบเริ่มต้น

ในส่วนที่แล้ว เราสร้างการจำลองในลักษณะที่เมื่อมีการเรียกเมธอดที่เยาะเย้ย จะส่งคืนค่า "ว่าง" ลักษณะการทำงานนี้สามารถกำหนดค่าได้ คุณยังสามารถจัดเตรียมการใช้งาน org.mockito.stubbing.Answer ของคุณเองได้หาก Mockito จัดเตรียมไว้ให้ไม่เหมาะสม แต่อาจเป็นสัญญาณบ่งชี้ว่ามีบางอย่างผิดปกติเมื่อการทดสอบหน่วยมีความซับซ้อนเกินไป จำหลักการ KISS!

มาสำรวจข้อเสนอของคำตอบเริ่มต้นที่กำหนดไว้ล่วงหน้าของ Mockito:

  • RETURNS_DEFAULTS เป็นกลยุทธ์เริ่มต้น ไม่ควรกล่าวถึงอย่างชัดเจนเมื่อตั้งค่าการเยาะเย้ย

  • CALLS_REAL_METHODS ทำให้การเรียกใช้ unstubbed เรียกเมธอดจริง

  • RETURNS_SMART_NULLS หลีกเลี่ยง NullPointerException โดยส่งคืน SmartNull แทนที่จะเป็น null เมื่อใช้อ็อบเจ็กต์ที่ส่งคืนโดยการเรียกเมธอดที่ไม่ได้ตัดตอน คุณจะยังคงล้มเหลวด้วย NullPointerException แต่ SmartNull ช่วยให้คุณติดตามสแต็กได้ดียิ่งขึ้นด้วยบรรทัดที่เรียกใช้เมธอด unstubbed สิ่งนี้ทำให้คุ้มค่าที่จะมี RETURNS_SMART_NULLS เป็นคำตอบเริ่มต้นใน Mockito!

  • RETURNS_MOCKS พยายามคืนค่าปกติ "ว่าง" ก่อน จากนั้นเยาะเย้ย ถ้าเป็นไปได้ มิฉะนั้นจะเป็น null เกณฑ์ความว่างเปล่าแตกต่างจากที่เราได้เห็นก่อนหน้านี้เล็กน้อย: แทนที่จะคืน null สำหรับสตริงและอาร์เรย์ ม็อคที่สร้างด้วย RETURNS_MOCKS จะส่งคืนสตริงว่างและอาร์เรย์ว่างตามลำดับ

  • RETURNS_SELF มีประโยชน์สำหรับการเยาะเย้ยผู้สร้าง ด้วยการตั้งค่านี้ ม็อคจะคืนค่าอินสแตนซ์ของตัวเอง หากมีการเรียกเมธอดที่คืนค่าประเภทที่เท่ากับคลาส (หรือซูเปอร์คลาส) ของคลาสที่เยาะเย้ย

  • RETURNS_DEEP_STUBS ลึกกว่า RETURNS_MOCKS และสร้าง mocks ที่สามารถส่งคืน mocks จาก mocks จาก mocks เป็นต้น ตรงกันข้ามกับ RETURNS_MOCKS กฎความว่างเปล่าเป็นค่าเริ่มต้นใน RETURNS_DEEP_STUBS ดังนั้นจึงส่งคืน null สำหรับสตริงและอาร์เรย์:

 interface We { Are we(); } interface Are { So are(); } interface So { Deep so(); } interface Deep { boolean deep(); } ... We mock = mock(We.class, Mockito.RETURNS_DEEP_STUBS); when(mock.we().are().so().deep()).thenReturn(true); assertTrue(mock.we().are().so().deep());

การตั้งชื่อล้อเลียน

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

 PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());

Mockito จะบ่น แต่เนื่องจากเรายังไม่ได้ตั้งชื่อล้อเลียนอย่างเป็นทางการ เราจึงไม่รู้ว่าอันไหน:

 Wanted but not invoked: passwordEncoder.encode(<any string>);

มาตั้งชื่อพวกเขาโดยส่งสตริงในการก่อสร้าง:

 PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, "robustPasswordEncoder"); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, "weakPasswordEncoder"); verify(robustPasswordEncoder).encode(anyString());

ตอนนี้ข้อความแสดงข้อผิดพลาดมีความเป็นมิตรและชี้ให้เห็นถึง robustPasswordEncoder อย่างชัดเจน:

 Wanted but not invoked: robustPasswordEncoder.encode(<any string>);

Implementing Multiple Mock Interfaces

Sometimes, you may wish to create a mock that implements several interfaces. Mockito is able to do that easily, like so:

 PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);

Listening Invocations

A mock can be configured to call an invocation listener every time a method of the mock was called. Inside the listener, you can find out whether the invocation produced a value or if an exception was thrown.

 InvocationListener invocationListener = new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport report) { if (report.threwException()) { Throwable throwable = report.getThrowable(); // do something with throwable throwable.printStackTrace(); } else { Object returnedValue = report.getReturnedValue(); // do something with returnedValue System.out.println(returnedValue); } } }; PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().invocationListeners(invocationListener)); passwordEncoder.encode("1");

In this example, we're dumping either the returned value or a stack trace to a system output stream. Our implementation does roughly the same as Mockito's org.mockito.internal.debugging.VerboseMockInvocationLogger (don't use this directly, it's internal stuff). If logging invocations is the only feature you need from the listener, then Mockito provides a cleaner way to express your intent with the verboseLogging() setting:

 PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());

Take notice, though, that Mockito will call the listeners even when you're stubbing methods. พิจารณาตัวอย่างต่อไปนี้:

 PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging()); // listeners are called upon encode() invocation when(passwordEncoder.encode("1")).thenReturn("encoded1"); passwordEncoder.encode("1"); passwordEncoder.encode("2");

This snippet will produce an output similar to the following:

 ############ Logging method invocation #1 on mock/spy ######## passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) has returned: "null" ############ Logging method invocation #2 on mock/spy ######## stubbed: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:89) has returned: "encoded1" (java.lang.String) ############ Logging method invocation #3 on mock/spy ######## passwordEncoder.encode("2"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:90) has returned: "null"

Note that the first logged invocation corresponds to calling encode() while stubbing it. It's the next invocation that corresponds to calling the stubbed method.

การตั้งค่าอื่นๆ

Mockito offers a few more settings that let you do the following:

  • Enable mock serialization by using withSettings().serializable() .
  • Turn off recording of method invocations to save memory (this will make verification impossible) by using withSettings().stubOnly() .
  • Use the constructor of a mock when creating its instance by using withSettings().useConstructor() . When mocking inner non-static classes, add an outerInstance() setting, like so: withSettings().useConstructor().outerInstance(outerObject) .

If you need to create a spy with custom settings (such as a custom name), there's a spiedInstance() setting, so that Mockito will create a spy on the instance you provide, like so:

 UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name("coolService"));

When a spied instance is specified, Mockito will create a new instance and populate its non-static fields with values from the original object. That's why it's important to use the returned instance: Only its method calls can be stubbed and verified.

Note that, when you create a spy, you're basically creating a mock that calls real methods:

 // creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));

When Mockito Tastes Bad

It's our bad habits that make our tests complex and unmaintainable, not Mockito. For example, you may feel the need to mock everything. This kind of thinking leads to testing mocks instead of production code. Mocking third-party APIs can also be dangerous due to potential changes in that API that can break the tests.

Though bad taste is a matter of perception, Mockito provides a few controversial features that can make your tests less maintainable. Sometimes stubbing isn't trivial, or an abuse of dependency injection can make recreating mocks for each test difficult, unreasonable or inefficient.

Clearing Invocations

Mockito allows for clearing invocations for mocks while preserving stubbing, like so:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); UserRepository userRepository = mock(UserRepository.class); // use mocks passwordEncoder.encode(null); userRepository.findById(null); // clear clearInvocations(passwordEncoder, userRepository); // succeeds because invocations were cleared verifyZeroInteractions(passwordEncoder, userRepository);

Resort to clearing invocations only if recreating a mock would lead to significant overhead or if a configured mock is provided by a dependency injection framework and stubbing is non-trivial.

Resetting a Mock

Resetting a mock with reset() is another controversial feature and should be used in extremely rare cases, like when a mock is injected by a container and you can't recreate it for each test.

Overusing Verify

Another bad habit is trying to replace every assert with Mockito's verify() . It's important to clearly understand what is being tested: interactions between collaborators can be checked with verify() , while confirming the observable results of an executed action is done with asserts.

Mockito Is about Frame of Mind

Using Mockito is not just a matter of adding another dependency, it requires changing how you think about your unit tests while removing a lot of boilerplate.

With multiple mock interfaces, listening invocations, matchers and argument captors, we've seen how Mockito makes your tests cleaner and easier to understand, but like any tool, it must be used appropriately to be useful. Now armed with the knowledge of Mockito's inner workings, you can take your unit testing to the next level.