اختبارات المعاملات تتيح للمطورين اختبار كودهم بكفاءة بمجموعة متنوعة من القيم المدخلة. في مجال اختبارات JUnit، كان المستخدمون المتمرسون يتعاملون منذ فترة طويلة مع تعقيدات تنفيذ هذه الاختبارات. ولكن مع إصدار JUnit 5.7، يدخل عصر جديد من الاختبار المعاملات، يقدم للمطورين دعمًا أوليًا وقدرات محسنة. دعونا نغوص في الإمكانيات المثيرة التي يقدمها JUnit 5.7 للاختبار المعاملات!
أمثلة المعاملات من وثائق JUnit 5.7
دعونا نرى بعض الأمثلة من الوثائق:
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
@ParameterizedTest
@CsvSource({
"apple, 1",
"banana, 2",
"'lemon, lime', 0xF1",
"strawberry, 700_000"
})
void testWithCsvSource(String fruit, int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
assertEquals(5, str.length());
assertTrue(num >=1 && num <=2);
assertEquals(2, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
}
Annotation @ParameterizedTest
يجب أن يصاحبه واحدة من عدة annotations مقدمات تصف من أين تأخذ المعاملات. مصدر المعاملات يُشار إليه عادةً بـ “مزود البيانات”.
I will not dive into their detailed description here: the JUnit user guide does it better than I could, but allow me to share several observations:
- Annotation
@ValueSource
محدود لتقديم قيمة معلمة واحدة فقط. بمعنى آخر، لا يمكن أن يكون لطريقة الاختبار أكثر من argument واحد، والأنواع التي يمكن استخدامها محدودة أيضًا.أنواع يمكن استخدامها محدودة. - تم تناول تمرير عدة معاملات بطريقة ما بواسطة
@CsvSource
، الذي يفسر كل سلسلة إلى سجل يتم تمريره كـ arguments حقل بحقل. هذا يمكن أن يصبح صعب القراءة بسهولة مع سلاسل طويلة أو عدد كبير من الأargument. الأنواع التي يمكن استخدامها محدودة أيضًا – المزيد عن هذا لاحقًا. - جميع المصادر التي تُعلن عن القيم الفعلية في الت annotations مقيّدة بالقيم الثابتة في وقت الترجمة (Limitation of Java annotations, not JUnit).
@MethodSource
و@ArgumentsSource
يقدمان stream/collection من (غير م typed) n-tuples التي تُمرر كمعاملات للمنهجية. هناك أنواع مختلفة من القيم التي تدعم تمثيل تسلسل n-tuples، ولكن لا يوجد منها يضمن أنهم سيطابقون قائمة معاملات المنهجية. هذا النوع من المصدر يتطلب methods أو classes إضافية، ولكنه لا يضع قيودًا على أين وكيف يتم الحصول على بيانات الاختبار.
正如你所看到的، أنواع المصادر المتاحة تتراوح من البسيطة (سهلة الاستخدام، ولكن محدودة في الوظيفة) إلى الأكثر مرونة التي تتطلب مزيدًا من الكود للعمل.
- ملاحظة — هذا عادة علامة على تصميم جيد: القليل من الكود مطلوب للوظيفة الأساسية، و��加 extra complexity مبرر عندما يستخدم لتمكين حالة استخدام أكثر طلبًا.
ما لا يبدو أنه يناسب هذا المفترض من السهل إلى المرون، هو @EnumSource
. انظر هذا المثال غير trivial من مجموعات المعاملات الأربعة بـ 2 قيم لكل منها.
- ملاحظة — بينما يمرر
@EnumSource
قيمة enumerate كمعامل واحد للمنهجية، من الناحية النظرية، يتم تحسين الاختبار بواسطة حقول enumerate، مما لا يضع قيودًا على عدد المعاملات.
enum Direction {
UP(0, '^'),
RIGHT(90, '>'),
DOWN(180, 'v'),
LEFT(270, '<');
private final int degrees;
private final char ch;
Direction(int degrees, char ch) {
this.degrees = degrees;
this.ch = ch;
}
}
@ParameterizedTest
@EnumSource
void direction(Direction dir) {
assertEquals(0, dir.degrees % 90);
assertFalse(Character.isWhitespace(dir.ch));
int orientation = player.getOrientation();
player.turn(dir);
assertEquals((orientation + dir.degrees) % 360, player.getOrientation());
}
فكّر في هذا الأمر: القائمة الم编码ة مسبقًا تقيّد مرونتها بشدة (لا توجد بيانات خارجية أو مولدة)، بينما كمية الكود الإضافي المطلوبة لتحديد enum
تجعل هذا الخيار أكثر إطالة مقارنةً بالاختيار مثلاً @CsvSource
.
لكن هذا مجرد انطباع أولي. سنرى كيف يمكن أن يصبح هذا أنيقًا عند استخدام قوة enum
الحقيقية في Java.
- ملاحظة: هذا المقال لا يتعامل مع التحقق من
enum
التي هي جزء من كود الإنتاج الخاص بك. هذه، بالطبع، كان يجب إعلانها بغض النظر عن كيفية تحققك منها. بدلاً من ذلك، يركز على متى وكيف تعبر عن بيانات الفحص على شكل Enums.
متى تستخدمها
هناك حالات تكون فيها enum
أفضل من البدائل:
عدة معلمات لكل اختبار
عندما تحتاج إلى معلمة واحدة فقط، قد لا ترغب في تعقيد الأمور أكثر من @ValueSource
. لكن بمجرد أن تحتاج إلى عدة معلمات – مثلاً، مدخلات ونتائج متوقعة – يجب أن تلجأ إلى @CsvSource,
@MethodSource/@ArgumentsSource
أو @EnumSource
.
بصورة ما، يسمح لك enum
بـ “تسريب” أي عدد من الحقول.
إذن، عندما تحتاج إلى إضافة معلمات إضافية إلى طريقة الاختبار في المستقبل، يمكنك ببساطة إضافة حقول إضافية في enum
الموجود، دون触碰 توقيعات الطرق الاختبارية. يصبح هذا الثمن ثمينًا عندما تستخدم مزود البيانات في اختبارات متعددة.
للمصادر الأخرى، يجب استخدام ArgumentsAccessor
أو ArgumentsAggregator
للحصول على المرونة التي تتمتع بها الأنواع المحددة مسبقًا بشكل افتراضي.
الأمان النوعي
بالنسبة لمطورين Java، يجب أن يكون هذا أمرًا مهمًا.
ال 매개 변수 التي تُقرأ من ملفات CSV (أو النصوص) أو @MethodSource
أو @ArgumentsSource
، لا توفر أي ضمان في وقت الترجمة أن عدد ال 매개 변수 وأنواعها ستطابق التوقيع.
من الواضح أن JUnit سيتشكو عند تشغيل الوقت، ولكن ان забудьте عن أي مساعدة من IDE الخاص بك.
نفس الشيء كما ذكرنا سابقًا، هذا يضاف عند إعادة استخدام نفس ال 매개 변수 في اختبارات متعددة. استخدام نهج آمن النوع سيكون فوزًا كبيرًا عند توسيع مجموعة ال 매개 변수 في المستقبل.
الأنواع المخصصة
هذا هو преимущеگی كبير على المصادر النصية، مثل المصادر التي تقرأ البيانات من ملفات CSV – القيم الم codified في النص يجب تحويلها إلى أنواع Java.
إذا كان لديك فئة مخصصة لإنشائها من تسجيل CSV، يمكنك القيام بذلك باستخدام ArgumentsAggregator
. ومع ذلك، إعلان بياناتك لا يزال غير آمن النوع – أي عدم تطابق بين توقيع الطريقة وبيانات المعلنة سيظهر عند تشغيل الوقت عند “جمع” ال 매개 변수. ناهيك عن أن إعلان فئة الم聚合er يضيف المزيد من الكود الداعم المطلوب لعملparameterization الخاص بك. وكنا دائمًا نفضل @CsvSource
على @EnumSource
لتجنب الكود الإضافي.
يمكن توثيقه
على عكس الطرق الأخرى، يحتوي مصدر التعداد على رموز Java لكل من مجموعتي المعلمات (الحالات التعدادية) وجميع المعلمات التي تحتويها (الحقول التعدادية). يوفرون مكانًا مباشرًا حيث يمكن إرفاق الوثائق بصورة أكثر طبيعية – JavaDoc.
ليس أن الوثائق لا يمكن وضعها في مكان آخر، ولكن ستكون – بحكم التعريف – موضوعة بعيدًا عن ما توثقه وبالتالي أصعب للعثور عليها، وأسهل في الإهمال.
ولكن هناك المزيد!
الآن: التعدادات. هي. طبقات.
يبدو أن الكثير من مطوري Java المبتدئين لم يدركوا بعد مدى قوة التعدادات التعدادية.
في لغات البرمجة الأخرى، هي في الحقيقة مجرد ثوابت محتفى بها. ولكن في Java، هي تنفيذات صغيرة مريحة لـنموذج تصميم Flyweight مع (الكثير من) مزايا الطبقات الكاملة.
لماذا هذا شيء جيد؟
السلوك ذات الصلة بشرط الاختبار
كما هو الحال مع أي فئة أخرى، يمكن إضافة الأساليب إلى التعدادات.
هذا يصبح مفيدًا إذا تم إعادة استخدام معلمات اختبار التعداد بين الاختبارات – نفس البيانات، فقط اختبارها بشكل مختلف قليلاً. للعمل بفعالية مع المعلمات دون نسخ ولصق كبير، يجب مشاركة بعض الكود المساعد بين تلك الاختبارات أيضًا.
هذا ليس شيئًا لن يحله فئة مساعدة وبضعة طرق ثابتة.
- ملاحظة: لاحظ أن هذا التصميم يعاني من حب للخصائص. يجب على طرق الاختبار أو الأسوأ من ذلك، طرق الفئة المساعدة، سحب البيانات من الكائنات المت枚举ة لتنفيذ إجراءات على تلك البيانات.
بينما هذا هو الطريقة (الوحيدة) في البرمجة الإجرائية، في عالم البرمجة الكائنية، يمكننا أن نفعل أفضل.
declaring the “helper” methods right in the enum declaration itself, we would move the code where the data is. Or, to put in OOP lingo, the helper methods would become the “behavior” of the test fixtures implemented as enums. This would not only make the code more idiomatic (calling sensible methods on instances over static methods passing data around), but it would also make it easier to reuse enum parameters across test cases.
الوراثة
يمكن للكائنات المت枚举ة تنفيذ واجهات تحتوي على طرق (افتراضية). عند استخدامها بشكل الحكيم، يمكن الاستفادة منها لتحويل السلوك بين عدة مقدمي بيانات – عدة كائنات مت枚举ة.
مثال يسهل التفكير فيه هو كائنات مت枚举ة منفصلة للاختبارات الإيجابية والسلبية. إذا كانوا يمثلون نوعًا مماثلاً من معدات الاختبار، فإنه من المحتمل أنهم يشاركون بعض السلوكيات.
الحديث رخيص
دعونا نوضح هذا في مجموعة اختبارات لمحول افتراضي للملفات المصدرية، ليس مختلفًا كثيرًا عن الذي يقوم بتحويل بايثون 2 إلى 3.
لتحقيق الثقة الحقيقية فيما يقوم به هذا الأداة الشاملة، ستنتهي بإنشاء مجموعة كبيرة من ملفات الإدخال التي تبرز جوانب مختلفة من اللغة، وملفات مطابقة لتحديد نتيجة التحويل against. باستثناء ذلك، من الضروري التحقق من التحذيرات/ال أخطاء التي تقدمها إلى المستخدم لمدخلات مشكوك فيها.
هذا يتناسب بشكل طبيعي مع الاختبارات المعلمة بسبب عدد كبير من العينات للتحقق، ولكن لا يتناسب تمامًا مع أي من مصادر المعاملات البسيطة في JUnit، لأن البيانات معقدة إلى حد ما.
انظر أدناه:
enum Conversion {
CLEAN("imports-correct.2.py", "imports-correct.3.py", Set.of()),
WARNINGS("problematic.2.py", "problematic.3.py", Set.of(
"Using module 'xyz' that is deprecated"
)),
SYNTAX_ERROR("syntax-error.py", new RuntimeException("Syntax error on line 17"));
// هناك العديد و العديد من الآخرين ...
@Nonnull
final String inFile;
@CheckForNull
final String expectedOutput;
@CheckForNull
final Exception expectedException;
@Nonnull
final Set expectedWarnings;
Conversion(@Nonnull String inFile, @Nonnull String expectedOutput, @NotNull Set expectedWarnings) {
this(inFile, expectedOutput, null, expectedWarnings);
}
Conversion(@Nonnull String inFile, @Nonnull Exception expectedException) {
this(inFile, null, expectedException, Set.of());
}
Conversion(@Nonnull String inFile, String expectedOutput, Exception expectedException, @Nonnull Set expectedWarnings) {
this.inFile = inFile;
this.expectedOutput = expectedOutput;
this.expectedException = expectedException;
this.expectedWarnings = expectedWarnings;
}
public File getV2File() { ... }
public File getV3File() { ... }
}
@ParameterizedTest
@EnumSource
void upgrade(Conversion con) {
try {
File actual = convert(con.getV2File());
if (con.expectedException != null) {
fail("No exception thrown when one was expected", con.expectedException);
}
assertEquals(con.expectedWarnings, getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV3File());
} catch (Exception ex) {
assertTypeAndMessageEquals(con.expectedException, ex);
}
}
استخدام الأنواع المحددة لا يقييدنا في مدى تعقيد البيانات التي يمكن أن تكون. كما ترون، يمكننا تعريف عدة بنائين مريحين في الأنواع المحددة، لذا يكون إعلان مجموعات المعاملات جديدة نظيفة وسهلة. هذا يمنع استخدام قوائم المعاملات الطويلة التي غالبًا ما تكون ممتلئة بالعديد من القيم “ال فارغة” (null، سلاسل فارغة، أو مجموعات) تتركك تتساءل عن ما يمثل المعامل رقم 7 – تعلم، أحد القيم null – بالفعل.
لاحظ كيف أن الأنواع المحددة تتيح استخدام أنواع معقدة (Set
، RuntimeException
) دون قيود أو تحويلات سحرية. تمرير هذه البيانات آمن تمامًا من الناحية النوعية.
الآن، أعلم ما تفكر فيه. هذا كثير الكلام. حسنًا، حتى نقطة معينة. واقعيًا، ستحصل على عدد أكبر من عينات البيانات للتحقق، لذا ستكون كمية الكود التقليدي أقل أهمية مقارنة بالأداء.
بالإضافة إلى ذلك، انظر كيف يمكن كتابة اختبارات مرتبطة باستخدام نفس الأنواع المحددة، وطرقها المساعدة:
@ParameterizedTest
@EnumSource
// ترقية الملفات التي تم ترقيتها بالفعل دائمًا تمر بنجاح، ولا تقوم بتحديثات، ولا تصدر تحذيرات.
void upgradeFromV3toV3AlwaysPasses(Conversion con) throws Exception {
File actual = convert(con.getV3File());
assertEquals(Set.of(), getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV3File());
}
@ParameterizedTest
@EnumSource
// خفض إصدار الملفات التي أنشأتها عملية الترقية من المتوقع دائمًا أن يمر بنجاح دون تحذيرات.
void downgrade(Conversion con) throws Exception {
File actual = convert(con.getV3File());
assertEquals(Set.of(), getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV2File());
}
المزيد من الحديث بعد كل شيء
من الناحية المفاهيمية، @EnumSource
يحثك على إنشاء وصف معقد و machine-readable لكل سيناريو اختباري، مما يبعد الخط الفاصل بين مزودي البيانات وال fixtures الاختبارية.
أحد الأشياء الرائعة في وجود كل مجموعة بيانات معبراً عنها كرمز Java (عنصر枚举) هو أن它们 يمكن استخدامها بشكل فردي؛ تماماً خارج مزودي البيانات/اختبارات المعلمة. Since они تحمل أسماء معقولة وهي محتواة (من ناحية البيانات والسلوك)، فإنها تساهم في إجراء اختبارات جيدة وقابلة للقراءة.
@Test
void warnWhenNoEventsReported() throws Exception {
FixtureXmls.Invalid events = FixtureXmls.Invalid.NO_EVENTS_REPORTED;
// read() هي دالة مساعدة تُستخدم من قبل جميع FixtureXmls
try (InputStream is = events.read()) {
EventList el = consume(is);
assertEquals(Set.of(...), el.getWarnings());
}
}
الآن، @EnumSource
لن يكون أحد أكثر مصادر الأргументات استخدامًا، وهذا أمر جيد، لأن الاستخدام المفرط له لن يفيد. ولكن في الظروف المناسبة، يأتي في متناول اليد معرفة كيفية استخدام كل ما يقدمونه.
Source:
https://dzone.com/articles/junit5-parameterized-tests-with-enumsource