هناك العديد من الأدوات في النظام البيئي PHP التي توفر اختبار PHP مريحة. أحد أشهرها هو
PHPUnit ، وهو مرادف تقريبًا للاختبار في هذه اللغة. ومع ذلك ، لا يتم كتابة الكثير عن طرق الاختبار الجيدة. هناك العديد من الخيارات لسبب ومتى تكتب الاختبارات ونوع الاختبارات وما إلى ذلك. ولكن بصراحة ،
ليس من المنطقي أن تكتب اختبارًا إذا لم تتمكن من قراءته لاحقًا .
الاختبارات هي نوع خاص من الوثائق. كما
كتبت عن TDD في PHP في وقت سابق ، فإن الاختبار دائما (أو على الأقل ينبغي) أن يحدد بوضوح مهمة جزء معين من الكود.
إذا تعذر على اختبار ما التعبير عن هذه الفكرة ، فسيكون الاختبار سيئًا.
أعددت مجموعة من التقنيات التي ستساعد مطوري PHP على كتابة اختبارات جيدة وقابلة للقراءة ومفيدة.
لنبدأ بالأساسيات
هناك مجموعة من التقنيات القياسية التي يتبعها الكثيرون دون أي أسئلة. سأذكر الكثير منهم وسأحاول شرح سبب الحاجة إليهم.
1. لا ينبغي أن تحتوي الاختبارات على عمليات الإدخال والإخراج
السبب الرئيسي : عمليات الإدخال / الإخراج بطيئة وغير موثوق بها.
بطيء : حتى إذا كان لديك أفضل الأجهزة في العالم ، فسيظل الإدخال / الإخراج أبطأ من عمليات الوصول إلى الذاكرة. يجب أن تعمل الاختبارات دائمًا بسرعة ، وإلا فلن يديرها الأشخاص إلا نادراً.
لا يمكن الاعتماد عليها : قد لا تتوفر بعض الملفات والثنائيات والمآخذ والمجلدات وسجلات DNS على بعض الأجهزة التي تختبرها. كلما زاد اعتمادك على اختبار I / O ، كلما كانت اختباراتك مرتبطة بالبنية التحتية.
ما هي العمليات المتعلقة I / O:
- قراءة وكتابة الملفات.
- مكالمات الشبكة.
- المكالمات إلى العمليات الخارجية (باستخدام
exec
، proc_open
، إلخ).
هناك حالات عندما يسمح لك وجود عمليات الإدخال والإخراج بكتابة الاختبارات بشكل أسرع. لكن كن حذرًا: تحقق من أن هذه العمليات تعمل بنفس الطريقة على أجهزتك من أجل التطوير والتجميع والنشر ، وإلا فقد تواجه مشكلات خطيرة.
عزل الاختبارات بحيث لا تحتاج إلى عمليات إدخال / إخراج: لقد قدمت حلاً معماريًا أدناه يمنع الاختبارات من إجراء عمليات الإدخال / الإخراج من خلال مشاركة المسؤولية بين الواجهات.
مثال:
public function getPeople(): array { $rawPeople = file_get_contents( 'people.json' ) ?? '[]'; return json_decode( $rawPeople, true ); }
عند بدء الاختبار باستخدام هذه الطريقة ، سيتم إنشاء ملف محلي ، وسيتم إنشاء لقطات من وقت لآخر:
public function testGetPeopleReturnsPeopleList(): void { $people = $this->peopleService ->getPeople();
للقيام بذلك ، نحتاج إلى تهيئة المتطلبات الأساسية لتشغيل الاختبارات. للوهلة الأولى ، يبدو كل شيء معقولًا ، لكنه في الحقيقة أمر فظيع.
تخطي الاختبار بسبب عدم تلبية المتطلبات الأساسية لا يضمن جودة برنامجنا. هذا سوف يخفي البق فقط!
نحن نصلح الموقف : نعزل عمليات الإدخال / الإخراج عن طريق تحويل المسؤولية إلى الواجهة.
الآن أعرف أن
JsonFilePeopleProvider
سيستخدم I / O في أي حال.
بدلاً من
file_get_contents()
يمكنك استخدام طبقة تجريدية مثل
نظام ملفات Flysystem ، الذي يسهل عليك إجراء الروتين.
ثم لماذا نحتاج إلى خدمة
PeopleService
؟ سؤال جيد لهذا ، هناك حاجة إلى اختبارات: لتحدي الهيكل وإزالة الكود غير المجدي.
2. يجب أن تكون الاختبارات واعية وذات مغزى.
السبب الرئيسي : الاختبارات هي شكل من أشكال الوثائق. اجعلها واضحة وموجزة وقابلة للقراءة.
الوضوح والإيجاز : لا توجد فوضى ، لا يوجد ألف سطر من الرهانات ، ولا تسلسل من البيانات.
سهولة القراءة : يجب أن تحكي الاختبارات قصة. هيكل "معين ، ومتى ، ثم" ممتاز لهذا.
خصائص اختبار جيد وقابل للقراءة:
- يحتوي فقط على المكالمات الضرورية لطريقة
assert
(يفضل واحدة).
- يشرح بوضوح ما يجب أن يحدث في ظل ظروف معينة.
- يختبر فرع واحد فقط من طريقة التنفيذ.
- إنه لا يصنع ركلة من أجل الكون كله من أجل أي بيان.
من المهم ملاحظة أنه إذا كان تطبيقك يحتوي على تعبيرات شرطية أو عوامل تشغيل انتقالية أو حلقات ، فيجب أن تكون جميعها مشمولة صراحةً في الاختبارات. على سبيل المثال ، بحيث تحتوي الإجابات المبكرة دائمًا على اختبار.
أكرر: إنها ليست مسألة تغطية ، ولكن مسألة توثيق.
فيما يلي مثال للاختبار المحير:
public function testCanFly(): void { $noWings = new Person(0); $this->assertEquals( false, $noWings->canFly() ); $singleWing = new Person(1); $this->assertTrue( !$singleWing->canFly() ); $twoWings = new Person(2); $this->assertTrue( $twoWings->canFly() ); }
لنعدّل التنسيق "معطى متى ، ثم" ونرى ما سيحدث:
public function testCanFly(): void {
مثل قسم "المقدم" ، يمكن نقل "متى" و "بعد ذلك" إلى طرق خاصة. هذا سيجعل اختبارك أكثر قابلية للقراءة.
assertEquals
فوضى لا معنى لها. يجب على الشخص الذي يقرأ هذا تتبع العبارة من أجل فهم معنى ذلك.
باستخدام عبارات محددة سيجعل اختبارك أكثر قابلية للقراءة. يجب أن تتلقى
assertTrue()
متغير Boolean ، وليس تعبيرًا مثل
canFly() !== true
.
في المثال السابق ، استبدلنا
assertEquals
بين
false
و
$person->canFly()
مع
assertFalse
بسيط:
الآن كل شيء واضح جدا! إذا كان الشخص ليس لديه أجنحة ، فلا يجب أن يكون قادرًا على الطيران! اقرأ مثل القصيدة
الآن قسم "الحالات الإضافية" ، الذي يظهر مرتين في نصنا ، هو إشارة واضحة إلى أن الاختبار يقدم الكثير من البيانات. طريقة
testCanFly()
غير مجدية بالكامل.
لنقم بتحسين الاختبار مرة أخرى:
public function testCanFlyIsFalsyWhenPersonHasNoWings(): void { $person = $this->givenAPersonHasNoWings(); $this->assertFalse( $person->canFly() ); } public function testCanFlyIsTruthyWhenPersonHasTwoWings(): void { $person = $this->givenAPersonHasTwoWings(); $this->assertTrue( $person->canFly() ); }
يمكننا حتى إعادة تسمية طريقة الاختبار بحيث
testPersonCantFlyWithoutWings
مع السيناريو الحقيقي ، على سبيل المثال ، في
testPersonCantFlyWithoutWings
، ولكن كل شيء يناسبني على
testPersonCantFlyWithoutWings
.
3. يجب ألا يعتمد الاختبار على اختبارات أخرى
السبب الرئيسي : يجب تشغيل الاختبارات وتشغيلها بنجاح بأي ترتيب.
لا أرى أسبابًا كافية لإنشاء روابط بين الاختبارات. لقد طُلب مني مؤخرًا إجراء اختبار وظيفة تسجيل الدخول ، وسأقدمه هنا كمثال جيد.
الاختبار يجب:
- إنشاء رمز JWT لتسجيل الدخول.
- تنفيذ وظيفة تسجيل الدخول.
- الموافقة على تغيير الحالة.
كان مثل هذا:
public function testGenerateJWTToken(): void {
هذا سيء لعدة أسباب:
- لا يمكن لـ PHPUnit ضمان هذا الأمر للتنفيذ.
- يجب أن تكون الاختبارات قادرة على الترشح بشكل مستقل.
- الاختبارات المتوازية قد تفشل بشكل عشوائي.
أسهل طريقة للتغلب على هذا هي استخدام المعطى ، ومتى ، ثم المخطط. لذلك ستكون الاختبارات أكثر تفكيرًا ، وسوف يروون قصة ، ويوضحون بوضوح تبعياتهم ، ويشرحون الوظيفة التي يجري اختبارها.
public function testAmazingFeatureChangesState(): void {
نحتاج أيضًا إلى إضافة اختبارات للمصادقة ، إلخ. هذه البنية جيدة جدًا بحيث
يتم استخدام Behat افتراضيًا .
4. دائما تنفيذ التبعيات
السبب الرئيسي : لهجة سيئة للغاية - لخلق كعب للدولة العالمية. عدم القدرة على إنشاء كعب روتين للتبعيات لا يسمح باختبار الوظيفة.
تلميح مفيد:
أنسى فصول الحالة الثابتة والحالات الفردية . إذا كان فصلك يعتمد على شيء ما ، فاجعله حتى يمكن تنفيذه.
هنا مثال حزين:
class FeatureToggle { public function isActive( Id $feature ): bool { $cookieName = $feature->getCookieName();
كيف يمكنني اختبار هذه الإجابة المبكرة؟
هذا صحيح. لا مفر
لاختباره ، نحتاج إلى فهم سلوك فئة
Cookies
والتأكد من أنه يمكننا إعادة إنتاج كل البيئة المرتبطة به ، مما ينتج عنه إجابات معينة.
لا تفعل هذا.
يمكن تصحيح الموقف إذا قمت بتطبيق مثيل من
Cookies
كتبعية. سيبدو الاختبار كالتالي:
الشيء نفسه ينطبق على المفردات. لذا ، إذا كنت تريد أن تجعل كائنًا فريدًا ، فعليك تكوين حاقن التبعية بشكل صحيح ، بدلاً من استخدام نقش أحادي (معاكس). وإلا ،
setInstance()
طرقًا مفيدة فقط لحالات مثل
reset()
أو
setInstance()
. في رأيي ، هذا مجنون.
من الطبيعي تمامًا تغيير البنية لجعل الاختبار أسهل! وخلق طرق لتسهيل الاختبار ليست طبيعية.
5. أبدا اختبار الأساليب المحمية / الخاصة
السبب الرئيسي : أنها تؤثر على الطريقة التي نختبر بها الوظائف من خلال تحديد توقيع السلوك: في ظل هذا الشرط ، عندما أتدخل A ، أتوقع الحصول على B.
الأساليب الخاصة / المحمية ليست جزءًا من تواقيع الوظيفة .
لا أريد حتى إظهار طريقة "لاختبار" الطرق الخاصة ، لكنني سأعطي تلميحًا: يمكنك القيام بذلك فقط باستخدام واجهة برمجة تطبيقات
الانعكاس .
عاقب نفسك دائمًا بطريقة ما عندما تفكر في استخدام الانعكاس لاختبار الأساليب الخاصة! سيئة ، سيئة المطور!
بحكم التعريف ، يتم استدعاء الأساليب الخاصة داخليًا فقط. وهذا هو ، فهي ليست متاحة للجمهور. هذا يعني أن الأساليب العامة فقط من نفس الفئة يمكنها الاتصال بهذه الطرق.
إذا اختبرت كل الطرق العامة ، فأنت أيضًا تختبر كل الطرق الخاصة / المحمية . إذا لم يكن الأمر كذلك ، فقم بإزالة الطرق الخاصة / المحمية بحرية ، ولا أحد يستخدمها على أي حال.
نصائح متقدمة
اتمنى انك لست بالملل بعد ومع ذلك ، كان علي أن أتحدث عن الأساسيات. الآن سوف أشارك رأيي في كتابة الاختبارات والقرارات النظيفة التي تؤثر على عملية التطوير الخاصة بي.
أهم شيء لا أنسىه عند كتابة الاختبارات:
- الدراسات.
- ردود فعل سريعة.
- توثيق.
- إعادة بيع ديون.
- تصميم أثناء الاختبار.
1. الاختبارات في البداية ، وليس النهاية
القيم : الدراسة ، التغذية المرتدة السريعة ، الوثائق ، إعادة البناء ، التصميم أثناء الاختبار.
هذا هو أساس كل شيء. الجانب الأكثر أهمية ، والذي يشمل جميع القيم المدرجة. عندما تكتب الاختبارات مقدمًا ، فإن هذا يساعدك أولاً على فهم كيفية تنظيم مخطط "معين ، ومتى ، ثم". عند القيام بذلك ، تقوم أولاً بتوثيق ، والأهم من ذلك ، تذكر وتعيين متطلباتك كأهم الجوانب.
هل من الغريب أن نسمع عن اختبارات الكتابة قبل التنفيذ؟ وتخيل كم هو غريب تنفيذ شيء ما ، وعند اختبار لمعرفة ذلك ، كل تعبيراتك "معطى متى ، ثم" لا معنى لها.
أيضا ، هذا النهج سوف تحقق توقعاتك كل ثانيتين. يمكنك الحصول على ردود الفعل في أسرع وقت ممكن. مهما كانت كبيرة أو صغيرة تبدو الميزة.
تعتبر الاختبارات الخضراء منطقة مثالية لإعادة البناء. الفكرة الرئيسية: لا توجد اختبارات - لا إعادة بناء المساكن. إعادة البناء دون اختبارات أمر خطير.
أخيرًا ، سيظهر لك الهيكل "المحدد متى ، ثم" ، ما هي الواجهات التي يجب أن تكون طرقك وكيف يجب أن تتصرف. إن إبقاء الاختبار نظيفًا سيجبرك أيضًا على اتخاذ قرارات معمارية مختلفة باستمرار. سيضطر هذا إلى إنشاء مصانع وواجهات وتعطيل الميراث ، إلخ. ونعم ، سيصبح الاختبار أسهل!
إذا كانت الاختبارات الخاصة بك عبارة عن مستندات مباشرة تشرح كيفية عمل التطبيق ، فمن الضروري أن توضح ذلك.
2. أفضل بدون اختبارات من الاختبارات السيئة
القيم : الدراسة ، الوثائق ، إعادة البناء.
يفكر العديد من المطورين في الاختبارات بهذه الطريقة: سأكتب ميزة ، وسأقود إطار الاختبار حتى تغطي الاختبارات عددًا معينًا من الخطوط الجديدة ، وأرسلها إلى العملية.
يبدو لي أنك بحاجة إلى إيلاء المزيد من الاهتمام للموقف عندما يبدأ مطور جديد في العمل مع هذه الميزة.
ماذا ستخبر الاختبارات هذا الشخص؟غالبًا ما تكون الاختبارات مربكة إذا لم تكن الأسماء مفصلة بما فيه الكفاية. ما هو أكثر وضوحًا:
testCanFly
أو
testCanFlyReturnsFalseWhenPersonHasNoWings
؟
إذا كانت اختباراتك مجرد رمز فوضوي يجعل إطار العمل يغطي المزيد من الخطوط ، مع أمثلة لا معنى لها ، فقد حان الوقت للتوقف والتفكير فيما إذا كنت تريد كتابة هذه الاختبارات على الإطلاق.
حتى هذا الهراء مثل تخصيص
$a
و
$b
للمتغيرات ، أو تخصيص أسماء ليست لها علاقة باستخدام معين.
تذكر أن الاختبارات الخاصة بك عبارة عن مستندات حية تحاول توضيح كيف يجب أن يتصرف طلبك.
assertFalse($a->canFly())
يوثق كثيرًا. و
assertFalse($personWithNoWings->canFly())
بالفعل كثير جدًا.
3. تشغيل الاختبارات تدخلي
القيم : الدراسة ، وردود الفعل السريعة ، وإعادة البناء.
قبل البدء في العمل على الميزات ، قم بإجراء الاختبارات. إذا فشلت قبل أن تبدأ العمل ، فسوف تعرف ذلك
قبل كتابة الرمز ، ولن تضطر إلى قضاء دقائق ثمينة في تصحيح الاختبارات المكسورة التي لم تهتم بها حتى.
بعد حفظ الملف ، قم بإجراء الاختبارات. كلما اكتشفت أن هناك شيئًا ما قد انهار ، زادت سرعة إصلاحه والمضي قدمًا. إذا كان انقطاع سير العمل لحل مشكلة ما غير مثمر لك ، فتخيل أنه سيتعين عليك لاحقًا الرجوع إلى العديد من الخطوات إذا كنت لا تعرف المشكلة.
بعد الدردشة مع الزملاء لمدة خمس دقائق أو التحقق من الإشعارات من Github ، قم بإجراء الاختبارات. إذا خجلوا ، فأنت تعرف من أين توقفت. إذا كانت الاختبارات خضراء ، يمكنك الاستمرار في العمل.
بعد أي إعادة بناء ، حتى أسماء متغير ، قم بإجراء الاختبارات.
على محمل الجد ، تشغيل الاختبارات لعنة. بقدر ما تضغط على زر حفظ.
يمكن
PHPUnit Watcher القيام بذلك نيابة عنك ، وحتى إرسال الإخطارات!
4. اختبارات كبيرة - مسؤولية كبيرة
القيم : الدراسة ، إعادة البناء ، التصميم أثناء الاختبار.
من الناحية المثالية ، يجب أن يكون لكل فصل اختبار واحد. يجب أن يشمل هذا الاختبار جميع الطرق العامة في هذه الفئة ، وكذلك كل تعبير شرطي أو مشغل انتقال ...
يمكنك أن تأخذ شيئا مثل هذا:
- فئة واحدة = حالة اختبار واحدة.
- طريقة واحدة = واحد أو أكثر من الاختبارات.
- فرع بديل واحد (if / switch / try-catch / استثناء) = اختبار واحد.
لذلك ، سوف تحتاج إلى أربعة اختبارات لهذا الرمز البسيط:
لمزيد من الأساليب العامة لديك ، ستكون هناك حاجة لمزيد من الاختبارات.
لا أحد يحب قراءة الوثائق الطويلة. نظرًا لأن اختباراتك هي أيضًا مستندات ، فإن الحجم الصغير والجدوى سيزيد من جودتها وفائدتها.
إنها أيضًا إشارة مهمة إلى أن صفك يتراكم المسؤولية وأنه حان الوقت لإعادة تشكيله عن طريق نقل عدد من الوظائف إلى فئات أخرى ، أو إعادة تصميم النظام.
5. دعم مجموعة من الاختبارات لحل مشاكل الانحدار
القيم : الدراسة والتوثيق وردود الفعل السريعة.
النظر في الوظيفة:
function findById(string $id): object { return fromDb((int) $id); }
تعتقد أن شخصًا ما ينقل "10" ، ولكن في الواقع "10 موز" تنتقل. وهذا هو ، قيمتين تأتي ، ولكن واحدة لا لزوم لها. لديك خطأ.
ماذا ستفعل أولا؟ اكتب اختبارًا يميز مثل هذا السلوك الخاطئ !!!
public function testFindByIdAcceptsOnlyNumericIds(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage( 'Only numeric IDs are allowed.' ); findById("10 bananas"); }
بالطبع ، الاختبارات لا تنقل أي شيء. لكنك الآن تعرف ما يجب القيام به حتى تنقله. قم بتصحيح الخطأ وجعل الاختبارات باللون الأخضر ونشر التطبيق وكن سعيدًا.
احتفظ بهذا الاختبار معك. كلما كان ذلك ممكنًا ، في مجموعة من الاختبارات المصممة لحل مشاكل الانحدار.
هذا كل شئ! ردود فعل سريعة ، وإصلاح الأخطاء ، والوثائق ، رمز مقاومة الانحدار والسعادة.
الكلمة الأخيرة
الكثير مما ذكر أعلاه هو مجرد رأيي الشخصي ، تم تطويره خلال حياتي المهنية. هذا لا يعني أن النصيحة صحيحة أو خاطئة ، إنها مجرد رأي.