دليل ممارس اختبار الوحدة إلى 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 jar من مستودع Maven المركزي.
تقترب من موكيتو
تم تصميم اختبارات الوحدة لاختبار سلوك فئات أو طرق معينة دون الاعتماد على سلوك تبعياتهم. نظرًا لأننا نختبر أصغر "وحدة" من التعليمات البرمجية ، فلن نحتاج إلى استخدام تطبيقات فعلية لهذه التبعيات. علاوة على ذلك ، سنستخدم تطبيقات مختلفة قليلاً لهذه التبعيات عند اختبار سلوكيات مختلفة. يتمثل الأسلوب التقليدي المعروف في هذا الأمر في إنشاء تطبيقات "أبطأ" محددة لواجهة مناسبة لسيناريو معين. عادة ما يكون لمثل هذه التطبيقات منطق مشفر. كعب هو نوع من اختبار مزدوج. تشمل الأنواع الأخرى المنتجات المقلدة والسخرية والجواسيس والدمى وما إلى ذلك.
سنركز فقط على نوعين من أزواج الاختبار ، "السخرية" و "الجواسيس" ، حيث يتم توظيفهم بكثافة بواسطة Mockito.
السخرية
ما هو الاستهزاء؟ من الواضح أنه ليس المكان الذي تسخر فيه من زملائك المطورين. السخرية من اختبار الوحدة هي عندما تنشئ كائنًا ينفذ سلوك نظام فرعي حقيقي بطرق خاضعة للرقابة. باختصار ، يتم استخدام النماذج كبديل للتبعية.
باستخدام 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; } ... }
PasswordEncoder
public interface PasswordEncoder { String encode(String password); }
UserService
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());
طرق Stubbing
لا تكون السخافات الجديدة غير المعدلة مفيدة إلا في حالات نادرة. عادة ، نريد تكوين النموذج وتحديد ما يجب القيام به عندما يتم استدعاء طرق معينة من النموذج. هذا يسمى stubbing .
يقدم Mockito طريقتين للإثارة. الطريقة الأولى هي " عندما يتم استدعاء هذه الطريقة ، فافعل شيئًا ما." ضع في اعتبارك المقتطف التالي:
when(passwordEncoder.encode("1")).thenReturn("a");
يُقرأ تقريبًا مثل اللغة الإنجليزية: "عندما يتم استدعاء passwordEncoder.encode(“1”)
، قم بإرجاع a
."
الطريقة الثانية للإثارة هي مثل "افعل شيئًا عندما يُستدعى الأسلوب الوهمي باستخدام الوسيطات التالية". يصعب قراءة طريقة الإمساك هذه لأن السبب محدد في النهاية. انصح:
doReturn("a").when(passwordEncoder).encode("1");
سيقرأ المقتطف الذي يحتوي على طريقة stubbing هذه: "ارجع a
encode()
passwordEncoder
مع وسيطة من 1
"
تعتبر الطريقة الأولى مفضلة لأنها آمنة من النوع ولأنها أكثر قابلية للقراءة. ومع ذلك ، نادرًا ما تضطر إلى استخدام الطريقة الثانية ، على سبيل المثال عند إعاقة طريقة حقيقية للتجسس لأن الاتصال بها قد يكون له آثار جانبية غير مرغوب فيها.
دعنا نستكشف بإيجاز طرق stubbing التي يوفرها Mockito. سنقوم بتضمين كلتا الطريقتين في Stubbing في أمثلةنا.
إرجاع القيم
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");
يمكن أيضًا استخدام هذا النمط مع طرق stubbing الأخرى لتحديد نتائج المكالمات المتتالية.
إرجاع الردود المخصصة
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());
أنت على حق إذا كنت تعتقد أنه يبدو مرهقًا. يوفر 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 أن الاستثناء الذي يتم طرحه صالح لتلك الطريقة المعطلة المحددة وسيشتكي إذا لم يكن الاستثناء موجودًا في قائمة الاستثناءات المحددة للطريقة. ضع في اعتبارك ما يلي:
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
كواجهة وظيفية وإنشاء مثيل لها باستخدام lambda (وهو ما فعلناه في المثال). قد تبدو البنية الأقل إيجازًا كما يلي:
ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };
إذا كنت بحاجة إلى إنشاء مُطابق وسيطة يعمل مع الأنواع الأولية ، فهناك عدة طرق أخرى لذلك في org.mockito.ArgumentMatchers
:
- charThat (ArgumentMatcher <Character> المطابق)
- booleanThat (ArgumentMatcher <Boolean> المطابق)
- byteThat (مُطابق ArgumentMatcher <Byte>)
- shortThat (ArgumentMatcher <Short> matcher)
- intThat (ArgumentMatcher <Integer> المطابق)
- longThat (ArgumentMatcher <Long> matcher)
- floatThat (مُطابق ArgumentMatcher <Float>)
- doubleThat (أداة تطابق ArgumentMatcher <Double>)
الجمع بين الثقافين
لا يستحق دائمًا إنشاء مُطابق وسيطة مخصص عندما يكون الشرط معقدًا للغاية بحيث لا يمكن التعامل معه باستخدام المطابقات الأساسية ؛ أحيانًا يؤدي الجمع بين المطابقات إلى حل المشكلة. يوفر 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")
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()
النموذجية بقيمة الوسيطة المحددة a
. يرجى ملاحظة أن التحقق من الدعاء المظلل هو زائدة عن الحاجة ؛ الغرض من المقتطف السابق هو إظهار فكرة إجراء التحقق بعد حدوث بعض التفاعلات.
إذا قمنا بتغيير السطر الأخير ليكون لدينا حجة مختلفة - على سبيل المثال ، b
- سيفشل الاختبار السابق وسيشتكي Mockito من أن الاستدعاء الفعلي له حجج مختلفة ( b
بدلاً من المتوقع a
).
يمكن استخدام أدوات مطابقة الوسيطة للتحقق تمامًا مثل stubbing:
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 أيضًا بالتحقق من ترتيب المكالمة في مجموعة من النماذج. إنها ليست ميزة يتم استخدامها كثيرًا ، ولكنها قد تكون مفيدة إذا كان ترتيب الاستدعاءات مهمًا. ضع في اعتبارك المثال التالي:
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 واجهة برمجة تطبيقات سهلة القراءة وسهلة القراءة ، ولكن دعنا نستكشف بعض أعماله الداخلية لفهم حدوده وتجنب الأخطاء الغريبة.
دعنا نفحص ما يحدث داخل 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");
بسيط: نحن نطلق على طريقة stubbed. داخليًا ، يحفظ Mockito هذا الاستدعاء لمزيد من التحقق ويعيد إجابة الاستدعاء المتوقفة ؛ في حالتنا ، إنها السلسلة 1
.
في السطر الرابع ( verify(mock).encode(or(eq("a"), endsWith("b")));
) ، نطلب من Mockito التحقق من وجود استدعاء encode()
مع هؤلاء حجج محددة.
verify()
أولاً ، مما يحول الحالة الداخلية لـ Mockito إلى وضع التحقق. من المهم أن نفهم أن Mockito تحافظ على حالتها في ThreadLocal
. هذا يجعل من الممكن تنفيذ بناء جملة لطيف ولكن ، من ناحية أخرى ، يمكن أن يؤدي إلى سلوك غريب إذا تم استخدام إطار العمل بشكل غير صحيح (إذا حاولت استخدام أدوات مطابقة الحجة خارج التحقق أو stubbing ، على سبيل المثال).
إذن كيف يقوم 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 غير مناسبة ، ولكن قد يكون ذلك مؤشرًا على وجود خطأ ما عندما تصبح اختبارات الوحدة معقدة للغاية. تذكر مبدأ قبلة!
دعنا نستكشف عرض Mockito للإجابات الافتراضية المحددة مسبقًا:
RETURNS_DEFAULTS
هي الاستراتيجية الافتراضية ؛ لا يجدر ذكره صراحةً عند إعداد نموذج وهمي.CALLS_REAL_METHODS
يجعل الاستدعاءات غير المستبعدة تستدعي الأساليب الحقيقية.يتجنب
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
وتقوم بإنشاء نماذج يمكنها إرجاع نماذج من mock من 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());
تسمية Mock
يسمح لك Mockito بتسمية mock ، وهي ميزة مفيدة إذا كان لديك الكثير من النماذج في الاختبار وتحتاج إلى التمييز بينها. بعد قولي هذا ، قد تكون الحاجة إلى تسمية mocks أحد أعراض سوء التصميم. ضع في اعتبارك ما يلي:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());
سيشتكي Mockito ، لكن لأننا لم نقم بتسمية mocks رسميًا ، لا نعرف أي واحد:
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 anouterInstance()
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.