هناك ثلاث مراحل في TDD: الترتيب والتصرف والتأكيد (المعطى، متى، ثم في BDD). تتمتع مرحلة التأكيد بدعم كبير للأدوات، وقد تكون على دراية بـ AssertJ أو FEST-Assert أو Hamcrest. وهو على النقيض من مرحلة الترتيب. في حين أن ترتيب بيانات الاختبار غالبًا ما يكون أمرًا صعبًا ويتم تخصيص جزء كبير من الاختبار لها عادةً، فمن الصعب الإشارة إلى أداة تدعمه.
يحاول منسق الاختبار سد هذه الفجوة من خلال ترتيب مثيلات الفئات المطلوبة للاختبارات. تمتلئ المثيلات بقيم عشوائية زائفة تعمل على تبسيط عملية إنشاء بيانات الاختبار. يعلن المُختبر فقط عن أنواع الكائنات المطلوبة ويحصل على مثيلات جديدة تمامًا. عندما لا تكون القيمة العشوائية الزائفة لحقل معين جيدة بما فيه الكفاية، يجب تعيين هذا الحقل فقط يدويًا:
Product product = Arranger . some ( Product . class );
product . setBrand ( "Ocado" );
< dependency >
< groupId >com.ocadotechnology.gembus groupId >
< artifactId >test-arranger artifactId >
< version >1.6.3 version >
dependency >
testImplementation ' com.ocadotechnology.gembus:test-arranger:1.6.3 '
تحتوي فئة Arranger على عدة طرق ثابتة لإنشاء قيم عشوائية زائفة للأنواع البسيطة. كل واحد منهم لديه وظيفة تغليف لجعل المكالمات أسهل لـ Kotlin. بعض المكالمات المحتملة مذكورة أدناه:
جافا | كوتلين | نتيجة |
---|---|---|
Arranger.some(Product.class) | some | مثيل للمنتج مع جميع الحقول المملوءة بالقيم |
Arranger.some(Product.class, "brand") | some | مثيل لمنتج بدون قيمة لحقل العلامة التجارية |
Arranger.someSimplified(Category.class) | someSimplified | في مثيل الفئة، تم تقليل حجم حقول مجموعة النوع إلى 1 ويقتصر عمق شجرة الكائنات على 3 |
Arranger.someObjects(Product.class, 7) | someObjects | دفق بحجم 7 من مثيلات المنتج |
Arranger.someEmail() | someEmail() | سلسلة تحتوي على عنوان البريد الإلكتروني |
Arranger.someLong() | someLong() | رقم عشوائي زائف من النوع الطويل |
Arranger.someFrom(listOfCategories) | someFrom(listOfCategories) | إدخال من قائمة الفئات |
Arranger.someText() | someText() | سلسلة تم إنشاؤها من سلسلة ماركوف؛ افتراضيًا، إنها سلسلة بسيطة جدًا، ولكن يمكن إعادة تكوينها عن طريق وضع ملف "enMarkovChain" آخر في مسار الفصل الاختباري مع تعريف بديل، ويمكنك العثور على واحد تم تدريبه على مجموعة أدوات باللغة الإنجليزية هنا؛ راجع الملف المضمن في ملف "enMarkovChain" الخاص بالمشروع لمعرفة تنسيق الملف |
- | some | مثيل للمنتج مع ملء جميع الحقول بقيم عشوائية باستثناء name الذي تم تعيينه على "ليس عشوائيًا جدًا"، يمكن استخدام بناء الجملة هذا لتعيين أكبر عدد ممكن من حقول الكائن حسب الضرورة، ولكن يجب أن يكون كل كائن قابلاً للتغيير |
قد لا تكون البيانات العشوائية تمامًا مناسبة لكل حالة اختبار. غالبًا ما يكون هناك حقل واحد على الأقل مهم لهدف الاختبار ويحتاج إلى قيمة معينة. عندما تكون الفئة المرتبة قابلة للتغيير، أو أنها فئة بيانات Kotlin، أو أن هناك طريقة لإنشاء نسخة معدلة (على سبيل المثال Lombok's @Builder(toBuilder = true)) فما عليك سوى استخدام ما هو متاح. لحسن الحظ، حتى لو كان غير قابل للتعديل، يمكنك استخدام منسق الاختبار. هناك إصدارات مخصصة من أساليب some()
و someObjects()
التي تقبل معلمة من النوع Map
. تمثل المفاتيح الموجودة في هذه الخريطة أسماء الحقول بينما يقدم الموردون المقابلون القيم التي سيحددها لك منظم الاختبار في تلك الحقول، على سبيل المثال:
Product product = Arranger . some ( Product . class , Map . of ( "name" , () -> value ));
افتراضيًا، يتم إنشاء القيم العشوائية وفقًا لنوع الحقل. لا تتوافق القيم العشوائية دائمًا بشكل جيد مع ثوابت الفئة. عندما يحتاج الكيان دائمًا إلى الترتيب فيما يتعلق ببعض القواعد المتعلقة بقيم الحقول، يمكنك توفير منظم مخصص:
class ProductArranger extends CustomArranger < Product > {
@ Override
protected Product instance () {
Product product = enhancedRandom . nextObject ( Parent . class );
product . setPrice ( BigDecimal . valueOf ( Arranger . somePositiveLong ( 9_999L )));
return product ;
}
}
للتحكم في عملية إنشاء Product
نحتاج إلى تجاوز طريقة instance()
. داخل الطريقة يمكننا إنشاء مثيل Product
كما نريد. على وجه التحديد، قد نقوم بإنشاء بعض القيم العشوائية. للراحة، لدينا حقل enhancedRandom
في فئة CustomArranger
. في المثال الموضح، نقوم بإنشاء مثيل Product
بجميع الحقول التي تحتوي على قيم عشوائية زائفة، ولكن بعد ذلك نقوم بتغيير السعر إلى شيء مقبول في مجالنا. هذا ليس سلبيًا وأصغر من رقم 10 آلاف.
يتم التقاط ProductArranger
تلقائيًا (باستخدام الانعكاس) بواسطة Arranger واستخدامه عند طلب مثيل جديد Product
. لا يقتصر الأمر على المكالمات المباشرة مثل Arranger.some(Product.class)
فحسب، بل يتعلق أيضًا بالمكالمات غير المباشرة. بافتراض وجود فئة Shop
مع products
الميدانية من النوع List
. عند استدعاء Arranger.some(Shop.class)
، سيستخدم المنظم ProductArranger
لإنشاء جميع المنتجات المخزنة في Shop.products
.
يمكن تكوين سلوك منظم الاختبار باستخدام الخصائص. إذا قمت بإنشاء ملف arranger.properties
وحفظته في جذر مسار الفصل (عادةً ما يكون دليل src/test/resources/
)، فسيتم التقاطه وتطبيق الخصائص التالية:
arranger.root
يتم التقاط المنظمين المخصصين باستخدام الانعكاس. تعتبر جميع الفئات التي تمتد إلى CustomArranger
بمثابة ترتيبات مخصصة. يركز التفكير على حزمة معينة والتي تكون بشكل افتراضي com.ocado
. هذا ليس بالضرورة مناسبًا لك. ومع ذلك، باستخدام arranger.root=your_package
يمكن تغييره إلى your_package
. حاول أن تكون الحزمة محددة قدر الإمكان لأن وجود شيء عام (على سبيل المثال فقط com
وهو حزمة الجذر في العديد من المكتبات) سيؤدي إلى مسح مئات الفئات مما سيستغرق وقتًا ملحوظًا.arranger.randomseed
افتراضيًا، يتم دائمًا استخدام نفس البذرة لتهيئة منشئ القيم العشوائية الزائفة الأساسي. ونتيجة لذلك، فإن عمليات التنفيذ اللاحقة سوف تولد نفس القيم. لتحقيق العشوائية عبر عمليات التشغيل، أي البدء دائمًا بقيم عشوائية أخرى، من الضروري ضبط arranger.randomseed=true
.arranger.cache.enable
تتطلب عملية ترتيب المثيلات العشوائية بعض الوقت. إذا قمت بإنشاء عدد كبير من المثيلات ولم تكن بحاجة إلى أن تكون عشوائية تمامًا، فقد يكون تمكين ذاكرة التخزين المؤقت هو الحل الأمثل. عند التمكين، تقوم ذاكرة التخزين المؤقت بتخزين مرجع لكل مثيل عشوائي وفي مرحلة ما يتوقف منظم الاختبار عن إنشاء مثيلات جديدة ويعيد استخدام المثيلات المخزنة مؤقتًا بدلاً من ذلك. بشكل افتراضي، يتم تعطيل ذاكرة التخزين المؤقت.arranger.overridedefaults
يحترم منظم الاختبار التهيئة الافتراضية للحقل، أي عندما يكون هناك حقل تمت تهيئته بسلسلة فارغة، فإن المثيل الذي تم إرجاعه بواسطة منظم الاختبار يحتوي على السلسلة الفارغة في هذا الحقل. ليس هذا هو ما تحتاجه دائمًا في الاختبارات، خاصة عندما يكون هناك تقليد في المشروع لتهيئة الحقول ذات القيم الفارغة. لحسن الحظ، يمكنك إجبار منظم الاختبار على استبدال الإعدادات الافتراضية بقيم عشوائية. قم بتعيين arranger.overridedefaults
إلى true لتجاوز التهيئة الافتراضية.arranger.maxRandomizationDepth
يمكن لبعض بنيات بيانات الاختبار إنشاء أي سلاسل طولية من الكائنات التي تشير إلى بعضها البعض. ومع ذلك، لاستخدامها بشكل فعال في حالة اختبار، فمن الضروري التحكم في طول هذه السلاسل. افتراضيًا، يتوقف منظم الاختبار عن إنشاء كائنات جديدة عند المستوى الرابع من عمق التداخل. إذا كان هذا الإعداد الافتراضي لا يناسب حالات اختبار مشروعك، فيمكن تعديله باستخدام هذه المعلمة. عندما يكون لديك سجل Java يمكن استخدامه كبيانات اختبار، ولكنك تحتاج إلى تغيير واحد أو اثنين من حقوله، فإن فئة Data
مع طريقة النسخ الخاصة بها توفر الحل. يعد هذا مفيدًا بشكل خاص عند التعامل مع السجلات غير القابلة للتغيير والتي ليس لديها طريقة واضحة لتغيير حقولها مباشرةً.
تتيح لك طريقة Data.copy
إنشاء نسخة سطحية من السجل أثناء تعديل الحقول المطلوبة بشكل انتقائي. من خلال توفير خريطة لتجاوزات الحقول، يمكنك تحديد الحقول التي تحتاج إلى تغيير وقيمها الجديدة. تهتم طريقة النسخ بإنشاء مثيل جديد للسجل بقيم الحقول المحدثة.
يوفر عليك هذا الأسلوب إنشاء كائن سجل جديد يدويًا وتعيين الحقول بشكل فردي، مما يوفر طريقة ملائمة لإنشاء بيانات اختبار مع اختلافات طفيفة عن السجلات الموجودة.
بشكل عام، فئة البيانات وطريقة النسخ الخاصة بها تنقذان الموقف من خلال تمكين إنشاء نسخ سطحية من السجلات مع تغيير الحقول المحددة، مما يوفر المرونة والراحة عند العمل مع أنواع السجلات غير القابلة للتغيير:
Data . copy ( myRecord , Map . of ( "recordFieldName" , () -> "altered value" ));
عند اجتياز اختبارات مشروع برمجي، نادرًا ما يكون لدى المرء انطباع بأنه لا يمكن تنفيذه بشكل أفضل. في نطاق ترتيب بيانات الاختبار، هناك مجالان نحاول تحسينهما باستخدام أداة ترتيب الاختبار.
من الأسهل فهم الاختبارات عند معرفة نية منشئها، أي سبب كتابة الاختبار ونوع المشكلات التي يجب اكتشافها. لسوء الحظ، ليس من غير المعتاد رؤية الاختبارات تحتوي على عبارات القسم (المحددة) مثل العبارة التالية:
Product product = Product . builder ()
. withName ( "Some name" )
. withBrand ( "Some brand" )
. withPrice ( new BigDecimal ( "12.99" ))
. withCategory ( "Water, Juice & Drinks / Juice / Fresh" )
...
. build ();
عند النظر إلى مثل هذا الكود، من الصعب تحديد القيم ذات الصلة بالاختبار والتي يتم توفيرها فقط لتلبية بعض المتطلبات غير الفارغة. إذا كان الاختبار يتعلق بالعلامة التجارية، فلماذا لا تكتبه بهذه الطريقة:
Product product = Arranger . some ( Product . class );
product . setBrand ( "Some brand" );
من الواضح الآن أن العلامة التجارية مهمة. دعونا نحاول أن نخطو خطوة أخرى إلى الأمام. قد يبدو الاختبار بأكمله كما يلي:
//arrange
Product product = Arranger . some ( Product . class );
product . setBrand ( "Some brand" );
//act
Report actualReport = sut . createBrandReport ( Collections . singletonList ( product ))
//assert
assertThat ( actualReport . getBrand ). isEqualTo ( "Some brand" )
نحن نختبر الآن أنه تم إنشاء التقرير للعلامة التجارية "بعض العلامات التجارية". لكن هل هذا هو الهدف؟ من المنطقي أن نتوقع إنشاء التقرير لنفس العلامة التجارية التي تم تعيين المنتج المحدد لها. إذن ما نريد اختباره هو:
//arrange
Product product = Arranger . some ( Product . class );
//act
Report actualReport = sut . createBrandReport ( Collections . singletonList ( product ))
//assert
assertThat ( actualReport . getBrand ). isEqualTo ( product . getBrand ())
في حالة كون حقل العلامة التجارية قابلاً للتغيير ونخشى أن يقوم sut
بتعديله، فيمكننا تخزين قيمته في متغير قبل الانتقال إلى مرحلة الفعل واستخدامه لاحقًا للتأكيد. الاختبار سيكون أطول، لكن النية تبقى واضحة.
من الجدير بالذكر أن ما قمنا به للتو هو تطبيق للقيمة المولدة وإلى حد ما أنماط طريقة الإنشاء الموضحة في أنماط اختبار xUnit: كود اختبار إعادة البناء بواسطة Gerard Meszaros.
هل سبق لك أن غيرت شيئًا صغيرًا في كود الإنتاج وانتهى بك الأمر بحدوث أخطاء في عشرات الاختبارات؟ البعض منهم يبلغ عن فشل التأكيد، والبعض الآخر ربما يرفض التجميع. هذه هي رائحة كود جراحة البندقية التي أطلقت للتو النار على اختباراتك البريئة. حسنًا، ربما ليست بريئة جدًا حيث يمكن تصميمها بشكل مختلف، للحد من الأضرار الجانبية الناجمة عن التغيير البسيط. دعونا نحللها باستخدام مثال. لنفترض أن لدينا في مجالنا الفئة التالية:
class TimeRange {
private LocalDateTime start ;
private long durationinMs ;
public TimeRange ( LocalDateTime start , long durationInMs ) {
...
وأنه يستخدم في العديد من الأماكن. خاصة في الاختبارات، بدون منظم الاختبار، باستخدام عبارات مثل هذه: new TimeRange(LocalDateTime.now(), 3600_000L);
ماذا سيحدث إذا اضطررنا لبعض الأسباب المهمة إلى تغيير الفصل إلى:
class TimeRange {
private LocalDateTime start ;
private LocalDateTime end ;
public TimeRange ( LocalDateTime start , LocalDateTime end ) {
...
من الصعب للغاية التوصل إلى سلسلة من عمليات إعادة البناء التي تحول الإصدار القديم إلى الإصدار الجديد دون كسر جميع الاختبارات التابعة. والأرجح هو السيناريو الذي يتم فيه تعديل الاختبارات وفقًا لواجهة برمجة التطبيقات (API) الجديدة للفئة واحدًا تلو الآخر. وهذا يعني الكثير من العمل غير المثير تمامًا مع العديد من الأسئلة المتعلقة بالقيمة المطلوبة للمدة (هل يجب أن أقوم بتحويلها بعناية إلى end
نوع LocalDateTime أم أنها مجرد قيمة عشوائية مناسبة). ستكون الحياة أسهل بكثير مع منظم الاختبار. عندما يكون لدينا في جميع الأماكن التي لا تتطلب TimeRange
فارغة Arranger.some(TimeRange.class)
، فهو جيد للإصدار الجديد من TimeRange
كما كان بالنسبة للإصدار القديم. وهذا يتركنا مع تلك الحالات القليلة التي لا تتطلب TimeRange
عشوائيًا، ولكن بما أننا نستخدم بالفعل منظم الاختبار للكشف عن نية الاختبار، ففي كل حالة نعرف بالضبط القيمة التي يجب استخدامها TimeRange
.
لكن هذا ليس كل ما يمكننا القيام به لتحسين الاختبارات. من المفترض أنه يمكننا تحديد بعض فئات مثيل TimeRange
، على سبيل المثال النطاقات من الماضي، والنطاقات من المستقبل، والنطاقات النشطة حاليًا. يعد TimeRangeArranger
مكانًا رائعًا لترتيب ما يلي:
class TimeRangeArranger extends CustomArranger < TimeRange > {
private final long MAX_DISTANCE = 999_999L ;
@ Override
protected TimeRange instance () {
LocalDateTime start = enhancedRandom . nextObject ( LocalDateTime . class );
LocalDateTime end = start . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( start , end );
}
public TimeRange fromPast () {
LocalDateTime now = LocalDateTime . now ();
LocalDateTime end = now . minusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( end . minusHours ( Arranger . somePositiveLong ( MAX_DISTANCE )), end );
}
public TimeRange fromFuture () {
LocalDateTime now = LocalDateTime . now ();
LocalDateTime start = now . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( start , start . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE )));
}
public TimeRange currentlyActive () {
LocalDateTime now = LocalDateTime . now ();
LocalDateTime start = now . minusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
LocalDateTime end = now . plusHours ( Arranger . somePositiveLong ( MAX_DISTANCE ));
return new TimeRange ( start , end );
}
}
لا ينبغي إنشاء طريقة الإنشاء هذه مقدمًا، بل يجب أن تتوافق مع حالات الاختبار الموجودة. ومع ذلك، هناك احتمالات أن يقوم TimeRangeArranger
بتغطية جميع الحالات التي يتم فيها إنشاء مثيلات TimeRange
للاختبارات. ونتيجة لذلك، بدلاً من استدعاءات المنشئ مع العديد من المعلمات الغامضة، لدينا منظم بطريقة جيدة التسمية تشرح معنى المجال للكائن الذي تم إنشاؤه وتساعد في فهم نية الاختبار.
لقد حددنا مستويين من منشئي بيانات الاختبار عند مناقشة التحديات التي تم حلها بواسطة Test Arranger. لكي تكتمل الصورة، نحتاج أن نذكر واحدًا آخر على الأقل، وهو التركيبات. من أجل هذه المناقشة، يمكننا أن نفترض أن Fixture هي فئة مصممة لإنشاء هياكل معقدة من بيانات الاختبار. يركز المنسق المخصص دائمًا على فئة واحدة، ولكن في بعض الأحيان يمكنك ملاحظة تكرار مجموعات من فئتين أو أكثر في حالات الاختبار الخاصة بك. قد يكون ذلك المستخدم وحسابه البنكي. قد يكون هناك CustomArranger لكل واحد منهم، ولكن لماذا نتجاهل حقيقة أنهم غالبًا ما يجتمعون معًا. هذا هو الوقت الذي يجب أن نبدأ فيه بالتفكير في التركيبة. سيكون مسؤولاً عن إنشاء كل من حساب المستخدم والحساب البنكي (من المفترض باستخدام منظمين مخصصين مخصصين) وربطهما معًا. تم وصف التركيبات بالتفصيل، بما في ذلك العديد من متغيرات التنفيذ في أنماط اختبار xUnit: رمز اختبار إعادة البناء بواسطة Gerard Meszaros.
لذلك لدينا ثلاثة أنواع من اللبنات الأساسية في فصول الاختبار. يمكن اعتبار كل واحد منهم نظيرًا لمفهوم (كتلة بناء التصميم المستند إلى المجال) من كود الإنتاج:
على السطح، هناك البدائيون والأشياء البسيطة. وهذا شيء يظهر حتى في أبسط اختبارات الوحدة. يمكنك تغطية ترتيب بيانات الاختبار هذه باستخدام أساليب someXxx
من فئة Arranger
.
لذلك قد يكون لديك خدمات تتطلب اختبارات تعمل فقط على مثيلات User
أو كلاً User
والفئات الأخرى الموجودة في فئة User
، مثل قائمة العناوين. لتغطية مثل هذه الحالات، عادةً ما يلزم وجود منظم مخصص، على سبيل المثال UserArranger
. سيتم إنشاء مثيلات User
الذي يحترم جميع القيود وثوابت الفئة. علاوة على ذلك، فإنه سيختار AddressArranger
، في حالة وجوده، لملء قائمة العناوين بالبيانات الصالحة. عندما تتطلب حالات اختبار متعددة نوعًا معينًا من المستخدمين، على سبيل المثال، مستخدمين بلا مأوى لديهم قائمة عناوين فارغة، يمكن إنشاء طريقة إضافية في UserArranger. ونتيجة لذلك، عندما تكون هناك حاجة لإنشاء مثيل User
للاختبارات، سيكون كافيًا النظر في UserArranger
وتحديد طريقة المصنع المناسبة أو فقط الاتصال بـ Arranger.some(User.class)
.
وتتعلق الحالة الأكثر تحديًا بالاختبارات التي تعتمد على هياكل البيانات الكبيرة. في التجارة الإلكترونية، يمكن أن يكون هذا متجرًا يحتوي على العديد من المنتجات، ولكن أيضًا حسابات مستخدمين لها سجل تسوق. عادةً ما يكون ترتيب البيانات لحالات الاختبار هذه أمرًا غير تافه، ولن يكون من الحكمة تكرار مثل هذا الشيء. من الأفضل تخزينها في فئة مخصصة ضمن طريقة جيدة التسمية، مثل shopWithNineProductsAndFourCustomers
، وإعادة استخدامها في كل اختبار. نوصي بشدة باستخدام اصطلاح التسمية لهذه الفئات، لتسهيل العثور عليها، ونقترح استخدام Fixture
postfix. في نهاية المطاف، قد ينتهي بنا الأمر إلى شيء من هذا القبيل:
class ShopFixture {
Repository repo ;
public void shopWithNineProductsAndFourCustomers () {
Arranger . someObjects ( Product . class , 9 )
. forEach ( p -> repo . save ( p ));
Arranger . someObjects ( Customer . class , 4 )
. forEach ( p -> repo . save ( p ));
}
}
تم تجميع أحدث إصدار من منظم الاختبار باستخدام Java 17 ويجب استخدامه في وقت تشغيل Java 17+. ومع ذلك، يوجد أيضًا فرع Java 8 للتوافق مع الإصدارات السابقة، مغطى بالإصدارات 1.4.x.