كيف تبدأ أخيرًا في كتابة الاختبارات ولا تندم عليها



قادمًا إلى مشروع جديد ، أواجه بانتظام أحد المواقف التالية:

  1. لا توجد اختبارات على الإطلاق.
  2. هناك عدد قليل من الاختبارات ، ونادرًا ما تتم كتابتها ولا تعمل بشكل مستمر.
  3. الاختبارات موجودة ومضمنة في CI (التكامل المستمر) ، لكنها تضر أكثر مما تنفع.

لسوء الحظ ، فإن السيناريو الأخير هو الذي يؤدي غالبًا إلى محاولات جادة لبدء تنفيذ الاختبار في غياب المهارات المناسبة.

ما الذي يمكن فعله لتغيير الوضع الحالي؟ فكرة استخدام الاختبارات ليست جديدة. في الوقت نفسه ، تشبه معظم البرامج التعليمية الصورة الشهيرة حول كيفية رسم بومة: نحن نربط JUnit ، نكتب الاختبار الأول ، نستخدم أول وهمي! لا تجيب مثل هذه المقالات على أسئلة حول الاختبارات التي يجب كتابتها ، وما الذي يستحق الانتباه إليه وكيفية التعايش مع كل هذا. من هنا ولدت فكرة هذه المقالة. حاولت أن ألخص بإيجاز تجربتي في تنفيذ الاختبارات في مشاريع مختلفة من أجل تسهيل هذا المسار للجميع.


هناك ما يكفي من المقالات التمهيدية حول هذا الموضوع ، لذلك لن نكرر أنفسنا ونحاول الانتقال من الجانب الآخر. في الجزء الأول ، سنكشف عن الخرافة التي مفادها أن الاختبار ينطوي على تكاليف إضافية حصرية. سيتم توضيح كيف أن إنشاء اختبارات الجودة يمكن أن يؤدي بدوره إلى تسريع عملية التطوير. ثم ، على سبيل المثال من مشروع صغير ، سيتم النظر في المبادئ والقواعد الأساسية التي يجب اتباعها لتحقيق هذه الفائدة. أخيرًا ، في القسم الأخير ، سيتم إعطاء توصيات تنفيذ محددة: كيفية تجنب المشاكل النموذجية عندما تبدأ الاختبارات ، على العكس ، تبطئ التطور بشكل كبير.

نظرًا لأن تخصصي الرئيسي هو الواجهة الخلفية لـ Java ، فسيتم استخدام مكدس التقنية التالي في الأمثلة: Java و JUnit و H2 و Mockito و Spring و Hibernate. في الوقت نفسه ، تم تخصيص جزء كبير من المقالة لمشاكل الاختبار العامة والنصائح الواردة فيه تنطبق على مجموعة أكبر بكثير من المهام.

ومع ذلك ، كن حذرا! الاختبارات تسبب الإدمان: بمجرد أن تتعلم كيفية استخدامها ، لا يمكنك العيش بدونها.


الاختبارات مقابل سرعة التطوير


الأسئلة الرئيسية التي تطرأ عند مناقشة تنفيذ الاختبار: كم من الوقت سيستغرق كتابة الاختبارات وما الفوائد التي ستحصل عليها؟ سيتطلب الاختبار ، مثل أي تقنية أخرى ، جهودًا جادة للتطوير والتنفيذ ، لذلك في البداية لا ينبغي توقع فوائد كبيرة. أما بالنسبة لتكاليف الوقت ، فهي تعتمد بشكل كبير على فريق معين. ومع ذلك ، لا يجب حساب أقل من 20-30 ٪ من التكاليف الإضافية للتشفير بدقة. القليل ببساطة لا يكفي لتحقيق بعض النتائج على الأقل. غالبًا ما يكون توقع العوائد الفورية هو السبب الرئيسي للحد من هذا النشاط حتى قبل أن تصبح الاختبارات مفيدة.

ولكن ما هو نوع الكفاءة الذي نتحدث عنه؟ دعونا نسقط كلمات عن صعوبات التنفيذ ونرى ما هي الفرص المحددة لتوفير اختبار الوقت الذي يفتح.

كود التشغيل في أي مكان


إذا لم تكن هناك اختبارات في المشروع ، فإن الطريقة الوحيدة للبدء هي رفع التطبيق بالكامل. من الجيد أن يستغرق الأمر حوالي 15-20 ثانية ، ولكن حالات المشاريع الكبيرة التي يمكن أن يستغرق فيها الإطلاق الكامل من عدة دقائق ليست نادرة. ماذا يعني هذا للمطورين؟ سيكون جزءًا كبيرًا من وقت عملهم هو جلسات الانتظار القصيرة هذه ، والتي لا يمكنك خلالها الاستمرار في العمل في المهمة الحالية ، ولكن في الوقت نفسه ليس هناك الكثير من الوقت للتحول إلى شيء آخر. واجه الكثيرون مرة واحدة على الأقل مثل هذه المشاريع حيث يتطلب الرمز المكتوب في ساعة ساعات عديدة من التصحيح بسبب إعادة التشغيل الطويلة بين التصحيحات. في الاختبارات ، يمكنك تحديد تشغيل أجزاء صغيرة من التطبيق ، مما سيقلل بشكل كبير من وقت الانتظار ويزيد من إنتاجية العمل على التعليمات البرمجية.

بالإضافة إلى ذلك ، تؤدي القدرة على تشغيل التعليمات البرمجية في أي مكان إلى تصحيح أخطاء أكثر شمولاً. غالبًا ما يتطلب التحقق من حالات الاستخدام الإيجابي الرئيسية من خلال واجهة التطبيق جهدًا ووقتًا جديًا. إن وجود الاختبارات يجعل من الممكن إجراء فحص مفصل لوظيفة معينة أسهل وأسرع.

ميزة أخرى هي القدرة على تنظيم حجم الوحدة المختبرة. اعتمادًا على مدى تعقيد المنطق الذي يتم اختباره ، يمكنك تقييد نفسك بأسلوب واحد ، فئة ، مجموعة من الفئات التي تنفذ بعض الوظائف ، خدمة ، وما إلى ذلك ، حتى أتمتة اختبار التطبيق بالكامل. تتيح لك هذه المرونة تفريغ الاختبارات عالية المستوى من العديد من الأجزاء نظرًا لحقيقة أنها ستختبر على مستويات أقل.

إعادة اختبار


غالبًا ما يُشار إلى هذا الجمع باعتباره جوهر أتمتة الاختبار ، ولكن دعونا ننظر إليه من زاوية أقل دراية. ما هي الفرص الجديدة التي يفتحها للمطورين؟

أولاً ، سيتمكن كل مطور جديد جاء إلى المشروع من تشغيل الاختبارات الحالية بسهولة لفهم منطق التطبيق باستخدام الأمثلة. لسوء الحظ ، تم التقليل من أهمية هذا إلى حد كبير. في الظروف الحديثة ، نادرًا ما يعمل نفس الأشخاص في مشروع لمدة تزيد عن 1-2 سنة. وبما أن الفرق تتكون من عدة أشخاص ، فإن ظهور مشارك جديد كل 2-3 أشهر هو وضع نموذجي للمشاريع الكبيرة نسبيًا. تشهد المشاريع الصعبة بشكل خاص تحولات من أجيال كاملة من المطورين! القدرة على تشغيل أي جزء من التطبيق بسهولة وإلقاء نظرة على سلوك النظام في بعض الأحيان يبسط الانغماس المبرمجين الجدد في المشروع. بالإضافة إلى ذلك ، فإن دراسة أكثر تفصيلاً لمنطق الشفرة تقلل من عدد الأخطاء التي يتم ارتكابها في الإخراج والوقت اللازم لتصحيحها في المستقبل.

ثانيًا ، إن القدرة على التحقق بسهولة من أن التطبيق يعمل بشكل صحيح يفتح الطريق أمام إعادة البناء المستمر. هذا المصطلح ، للأسف ، أقل شعبية من CI. وهذا يعني أنه يمكن ويجب إعادة الهيكلة في كل مرة يتم فيها تنقيح الرمز. إنها مراعاة منتظمة لقاعدة الكشافة سيئة السمعة "اترك موقف السيارات نظيفًا أكثر مما كان عليه قبل وصولك" ، والذي يسمح بتجنب تدهور قاعدة الكود ويضمن للمشروع حياة طويلة وسعيدة.

تصحيح الأخطاء


تم بالفعل تصحيح التصحيح في الفقرات السابقة ، ولكن هذه النقطة مهمة للغاية لدرجة أنها تستحق نظرة فاحصة. لسوء الحظ ، لا توجد طريقة موثوقة لقياس العلاقة بين الوقت المستغرق في كتابة التعليمات البرمجية وتصحيحها ، نظرًا لأن هذه العمليات لا يمكن فصلها عن بعضها البعض عمليًا. ومع ذلك ، فإن وجود اختبارات الجودة في المشروع يقلل بشكل كبير من وقت التصحيح ، حتى الغياب شبه الكامل للحاجة إلى تشغيل المصحح.

الفعالية


كل ما سبق يمكن أن يوفر الوقت بشكل كبير على التصحيح الأولي للكود. مع النهج الصحيح ، هذا فقط سيدفع جميع تكاليف التطوير الإضافية. مكافآت الاختبار المتبقية - تحسين جودة قاعدة التعليمات البرمجية (يصعب اختبار التعليمات البرمجية سيئة التصميم) ، وتقليل عدد العيوب ، والقدرة على التحقق من صحة الشفرة في أي وقت ، وما إلى ذلك - ستصبح مجانية تقريبًا.

من النظرية إلى الممارسة


بالكلمات ، يبدو كل شيء جيدًا ، ولكن دعونا نبدأ العمل. كما ذكرنا سابقًا ، هناك أكثر من مواد كافية حول كيفية إجراء الإعداد الأولي لبيئة الاختبار. لذلك ، ننتقل على الفور إلى المشروع النهائي. مصادر هنا.

التحدي


كمهمة قالب ، ضع في اعتبارك جزءًا صغيرًا من الواجهة الخلفية لمتجر عبر الإنترنت. سنكتب واجهة برمجة تطبيقات نموذجية للعمل مع المنتجات: الإنشاء والاستلام والتحرير. بالإضافة إلى طريقتين للعمل مع العملاء: تغيير "المنتج المفضل" وحساب نقاط المكافأة للطلب.

نموذج المجال


حتى لا نفرط في تحميل المثال ، فإننا نقصر أنفسنا على الحد الأدنى من الحقول والفئات.



لدى العميل اسم مستخدم ورابط لمنتج مفضل وعلامة تشير إلى ما إذا كان عميلاً مميزًا.

المنتج (المنتج) - الاسم والسعر والخصم والعلم الذي يشير إلى ما إذا كان يتم الإعلان عنه حاليًا.

هيكل المشروع


هيكل رمز المشروع الرئيسي على النحو التالي.



الطبقات الطبقات:

  • نموذج - نموذج مجال المشروع ؛
  • Jpa - مستودعات للعمل مع قواعد البيانات المستندة إلى Spring Data ؛
  • الخدمة - منطق الأعمال للتطبيق ؛
  • وحدة التحكم - وحدات التحكم التي تنفذ API.

هيكل اختبار الوحدة.



توجد فصول الاختبار في الحزم نفسها الموجودة في الكود الأصلي. بالإضافة إلى ذلك ، تم إنشاء حزمة مع بناة لإعداد بيانات الاختبار ، ولكن المزيد عن ذلك أدناه.

من السهل فصل اختبارات الوحدة واختبارات التكامل. غالبًا ما يكون لها تبعيات مختلفة ، ومن أجل التطور المريح ، يجب أن تكون هناك القدرة على تشغيل أحدهما أو الآخر. يمكن تحقيق ذلك بطرق مختلفة: اصطلاحات التسمية والوحدات والحزم ومجموعات SourceSets. اختيار طريقة محددة هو مسألة ذوق. في هذا المشروع ، تكمن اختبارات التكامل في SourceSet منفصلة - IntegrTest.



مثل اختبارات الوحدات ، توجد الفصول التي تحتوي على اختبارات التكامل في الحزم نفسها الموجودة في الرمز الأصلي. بالإضافة إلى ذلك ، هناك فئات أساسية تساعد على التخلص من تكرار التكوين ، وإذا لزم الأمر ، تحتوي على طرق عالمية مفيدة.

اختبارات التكامل


هناك طرق مختلفة لما تستحق الاختبارات من البداية. إذا لم يكن المنطق الذي تم اختباره معقدًا للغاية ، يمكنك المتابعة فورًا إلى منطق التكامل (يُطلق عليها أحيانًا أيضًا قواعد القبول). على عكس اختبارات الوحدة ، يتأكدون من أن التطبيق ككل يعمل بشكل صحيح.

العمارة

تحتاج أولاً إلى أن تقرر ما هي فحوصات التكامل على مستوى معين سيتم إجراؤها. يوفر Spring Boot حرية اختيار كاملة: يمكنك رفع جزء من السياق والسياق بالكامل وحتى خادم كامل يمكن الوصول إليه من الاختبارات. كلما زاد حجم التطبيق ، تصبح هذه المشكلة أكثر تعقيدًا. غالبًا ما يتعين عليك كتابة اختبارات مختلفة على مستويات مختلفة.

ستكون نقطة البداية الجيدة هي اختبارات التحكم بدون بدء تشغيل الخادم. في التطبيقات الصغيرة نسبيًا ، من المقبول تمامًا رفع السياق بالكامل ، لأنه بشكل افتراضي يتم إعادة استخدامه بين الاختبارات ويتم تهيئته مرة واحدة فقط. خذ بعين الاعتبار الطرق الأساسية لفئة ProductController :

 @PostMapping("new") public Product createProduct(@RequestBody Product product) { return productService.createProduct(product); } @GetMapping("{productId}") public Product getProduct(@PathVariable("productId") long productId) { return productService.getProduct(productId); } @PostMapping("{productId}/edit") public void updateProduct(@PathVariable("productId") long productId, @RequestBody Product product) { productService.updateProduct(productId, product); } 

تركت مسألة معالجة الخطأ جانبا. افترض أنه تم تنفيذه خارجيًا بناءً على تحليل الاستثناءات المطروحة. كود الطرق بسيط للغاية ، وتنفيذها في ProductService ليس أكثر تعقيدًا:

 @Transactional(readOnly = true) public Product getProduct(Long productId) { return productRepository.findById(productId) .orElseThrow(() -> new DataNotFoundException("Product", productId)); } @Transactional public Product createProduct(Product product) { return productRepository.save(new Product(product)); } @Transactional public Product updateProduct(Long productId, Product product) { Product dbProduct = productRepository.findById(productId) .orElseThrow(() -> new DataNotFoundException("Product", productId)); dbProduct.setPrice(product.getPrice()); dbProduct.setDiscount(product.getDiscount()); dbProduct.setName(product.getName()); dbProduct.setIsAdvertised(product.isAdvertised()); return productRepository.save(dbProduct); } 

لا يحتوي مستودع ProductRepository على طرقه الخاصة على الإطلاق:

 public interface ProductRepository extends JpaRepository<Product, Long> { } 

يشير كل شيء إلى أن هذه الفئات لا تحتاج إلى اختبارات الوحدة لمجرد أنه يمكن التحقق من السلسلة بأكملها بسهولة وكفاءة من خلال العديد من اختبارات التكامل. يؤدي تكرار نفس الاختبارات في اختبارات مختلفة إلى تعقيد عملية التصحيح. في حالة حدوث خطأ في الرمز ، لن يسقط الآن اختبار واحد ، ولكن 10-15 على الفور. وهذا بدوره سيتطلب المزيد من التحليل. إذا لم يكن هناك ازدواج ، فمن المرجح أن يشير الاختبار الساقط الوحيد على الفور إلى خطأ.

التكوين

من أجل الراحة ، نسلط الضوء على BaseControllerIT الفئة الأساسية ، والتي تحتوي على تكوين الربيع وبعض الحقول:

 @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @Transactional public abstract class BaseControllerIT { @Autowired protected ProductRepository productRepository; @Autowired protected CustomerRepository customerRepository; } 

يتم نقل المستودعات إلى الفئة الأساسية حتى لا تزدحم فئات الاختبار. دورهم هو مساعد حصري: إعداد البيانات والتحقق من حالة قاعدة البيانات بعد عمل وحدة التحكم. عندما تزيد من حجم التطبيق ، قد لا يكون هذا مناسبًا ، ولكن كبداية سيكون مناسبًا تمامًا.

يتم تحديد التكوين الرئيسي للربيع بواسطة الخطوط التالية:

@SpringBootTest - يستخدم لتعيين سياق التطبيق. WebEnvironment.NONE يعني عدم الحاجة إلى إثارة سياق ويب.

@Transactional - يلتف جميع اختبارات الصف في معاملة مع التراجع التلقائي لحفظ حالة قاعدة البيانات.

هيكل الاختبار

دعنا ننتقل إلى مجموعة أضيق الحدود من الاختبارات لفئة ProductControllerIT - ProductControllerIT .

 @Test public void createProduct_productSaved() { Product product = product("productName").price("1.01").discount("0.1").advertised(true).build(); Product createdProduct = productController.createProduct(product); Product dbProduct = productRepository.getOne(createdProduct.getId()); assertEquals("productName", dbProduct.getName()); assertEquals(number("1.01"), dbProduct.getPrice()); assertEquals(number("0.1"), dbProduct.getDiscount()); assertEquals(true, dbProduct.isAdvertised()); } 

يجب أن يكون رمز الاختبار بسيطًا جدًا ومفهومًا في لمحة. إذا لم يكن الأمر كذلك ، فستفقد معظم مزايا الاختبارات الموضحة في القسم الأول من المقالة. من الممارسات الجيدة تقسيم جسم الاختبار إلى ثلاثة أجزاء يمكن فصلها بصريًا عن بعضها البعض: إعداد البيانات ، واستدعاء طريقة الاختبار ، والتحقق من صحة النتائج. في نفس الوقت ، من المستحسن جدًا أن يكون رمز الاختبار مناسبًا للشاشة ككل.

شخصيا ، يبدو لي أكثر وضوحا عندما يتم استخدام قيم الاختبار من قسم إعداد البيانات في وقت لاحق في عمليات الفحص. بدلاً من ذلك ، يمكنك مقارنة الكائنات بشكل صريح ، على سبيل المثال مثل هذا:

 assertEquals(product, dbProduct); 

في اختبار آخر لتحديث معلومات المنتج ( updateProduct ) ، من الواضح أن إنشاء البيانات قد أصبح أكثر تعقيدًا قليلاً وللحفاظ على السلامة المرئية للأجزاء الثلاثة من الاختبار ، يتم فصلها عن طريق خلاصتي سطر متتاليتين:

 @Test public void updateProduct_productUpdated() { Product product = product("productName").build(); productRepository.save(product); Product updatedProduct = product("updatedName").price("1.1").discount("0.5").advertised(true).build(); updatedProduct.setId(product.getId()); productController.updateProduct(product.getId(), updatedProduct); Product dbProduct = productRepository.getOne(product.getId()); assertEquals("updatedName", dbProduct.getName()); assertEquals(number("1.1"), dbProduct.getPrice()); assertEquals(number("0.5"), dbProduct.getDiscount()); assertEquals(true, dbProduct.isAdvertised()); } 

يمكن تبسيط كل جزء من الأجزاء الثلاثة للعجين. لإعداد البيانات ، يعتبر منشئو الاختبار ممتازين ، والذي يحتوي على منطق إنشاء كائنات مناسبة للاستخدام من الاختبارات. يمكن إجراء استدعاءات الطريقة المعقدة للغاية في طرق مساعدة داخل فئات الاختبار ، مع إخفاء بعض المعلمات غير ذات الصلة بهذه الفئة. لتبسيط الشيكات المعقدة ، يمكنك أيضًا كتابة وظائف مساعدة أو تنفيذ المطابقات الخاصة بك. الشيء الرئيسي مع كل هذه التبسيط هو عدم فقدان رؤية الاختبار: يجب أن يكون كل شيء واضحًا في لمحة عن الطريقة الرئيسية ، دون الحاجة إلى التعمق.

بناة الاختبار

يستحق بناة الاختبار اهتمامًا خاصًا. إن تغليف منطق إنشاء الكائنات يبسط صيانة الاختبار. على وجه الخصوص ، يمكن إخفاء ملء حقول النموذج غير ذات الصلة بهذا الاختبار داخل المنشئ. للقيام بذلك ، ليس عليك إنشائه مباشرة ، ولكن استخدم طريقة ثابتة تملأ الحقول المفقودة بالقيم الافتراضية. على سبيل المثال ، إذا ظهرت حقول جديدة مطلوبة في النموذج ، فيمكن إضافتها بسهولة إلى هذه الطريقة. في ProductBuilder يبدو الأمر كما يلي:

 public static ProductBuilder product(String name) { return new ProductBuilder() .name(name) .advertised(false) .price("0.00"); } 

اسم الاختبار

من الضروري فهم ما تم اختباره تحديدًا في هذا الاختبار. للتوضيح ، من الأفضل إعطاء إجابة لهذا السؤال في عنوانه. باستخدام اختبارات العينة لطريقة getProduct ضع في الاعتبار اصطلاح التسمية المستخدم:

 @Test public void getProduct_oneProductInDb_productReturned() { Product product = product("productName").build(); productRepository.save(product); Product result = productController.getProduct(product.getId()); assertEquals("productName", result.getName()); } @Test public void getProduct_twoProductsInDb_correctProductReturned() { Product product1 = product("product1").build(); Product product2 = product("product2").build(); productRepository.save(product1); productRepository.save(product2); Product result = productController.getProduct(product1.getId()); assertEquals("product1", result.getName()); } 

في الحالة العامة ، يتكون عنوان طريقة الاختبار من ثلاثة أجزاء ، مفصولة بتسطير: اسم الطريقة المختبرة ، والبرنامج النصي ، والنتيجة المتوقعة. ومع ذلك ، لم يقم أحد بإلغاء المنطق السليم ، وقد يكون هناك ما يبرر حذف بعض أجزاء الاسم إذا لم تكن هناك حاجة إليها في هذا السياق (على سبيل المثال ، نص برمجي في اختبار واحد لإنشاء منتج). الغرض من هذا التسمية هو التأكد من أن جوهر كل اختبار يمكن فهمه بدون تعلم الكود. وهذا يجعل نافذة نتائج الاختبار واضحة قدر الإمكان ، ومن خلالها يبدأ العمل في الاختبارات عادةً.



الاستنتاجات

هذا كل شيء. لأول مرة ، يكفي الحد الأدنى من أربعة اختبارات لاختبار طرق فئة ProductController . في حالة اكتشاف أخطاء ، يمكنك دائمًا إضافة الاختبارات المفقودة. في الوقت نفسه ، يقلل الحد الأدنى من الاختبارات بشكل كبير من الوقت والجهد لدعمها. هذا ، بدوره ، أمر بالغ الأهمية في عملية تنفيذ الاختبار ، حيث لا يتم الحصول على الاختبارات الأولى عادة من أفضل نوعية وتخلق العديد من المشاكل غير المتوقعة. في الوقت نفسه ، يكفي جناح الاختبار هذا للحصول على المكافآت الموضحة في الجزء الأول من المقالة.

تجدر الإشارة إلى أن هذه الاختبارات لا تتحقق من طبقة الويب للتطبيق ، ولكن هذا ليس مطلوبًا في كثير من الأحيان. إذا لزم الأمر ، يمكنك كتابة اختبارات منفصلة لطبقة الويب باستخدام كعب روتين بدلاً من القاعدة ( @WebMvcTest أو MockMvc أو @MockBean ) أو استخدام خادم كامل. يمكن للأخيرة تعقيد عملية التصحيح وتعقيد العمل بالمعاملات ، حيث لا يمكن للاختبار التحكم في معاملة الخادم. يمكن العثور على مثال لاختبار التكامل هذا في فئة CustomerControllerServerIT .

اختبارات الوحدة


تتميز اختبارات الوحدة بالعديد من المزايا مقارنة باختبارات التكامل:

  • يستغرق بدء التشغيل مللي ثانية ؛
  • حجم صغير للوحدة المختبرة ؛
  • من السهل تنفيذ التحقق من عدد كبير من الخيارات ، لأنه عندما يتم استدعاء الطريقة مباشرة ، يتم تبسيط إعداد البيانات بشكل كبير.

على الرغم من ذلك ، لا يمكن أن تضمن اختبارات الوحدة بطبيعتها قابلية تشغيل التطبيق ككل ولا تسمح لك بتجنب كتابة التكامل. إذا كان منطق الوحدة قيد الاختبار بسيطًا ، فلن يؤدي تكرار اختبارات التكامل مع اختبارات الوحدة إلى أي مزايا ، بل سيضيف فقط المزيد من التعليمات البرمجية إلى الدعم.

الفئة الوحيدة في هذا المثال التي تستحق اختبار الوحدة هي BonusPointCalculator . ميزتها المميزة هي عدد كبير من فروع منطق الأعمال. على سبيل المثال ، يفترض أن المشتري يحصل على مكافآت بنسبة 10 ٪ من تكلفة المنتج ، مضروبة في ما لا يزيد عن 2 مضاعفات من القائمة التالية:

  • المنتج يكلف أكثر من 10000 (× 4) ؛
  • يشارك المنتج في حملة إعلانية (× 3) ؛
  • المنتج هو المنتج "المفضل" للعميل (× 5) ؛
  • العميل لديه حالة ممتازة (× 2) ؛
  • إذا كان العميل لديه حالة ممتازة واشترى منتج "مفضل" ، بدلاً من المضاعفين المشار إليهما ، يتم استخدام واحد (× 8).

بالطبع ، في الحياة الواقعية ، سيكون من المفيد تصميم آلية عالمية مرنة لحساب هذه المكافآت ، ولكن لتبسيط المثال ، نقتصر على التنفيذ الثابت. يبدو رمز الحساب المضاعف كما يلي:

 private List<BigDecimal> calculateMultipliers(Customer customer, Product product) { List<BigDecimal> multipliers = new ArrayList<>(); if (customer.getFavProduct() != null && customer.getFavProduct().equals(product)) { if (customer.isPremium()) { multipliers.add(PREMIUM_FAVORITE_MULTIPLIER); } else { multipliers.add(FAVORITE_MULTIPLIER); } } else if (customer.isPremium()) { multipliers.add(PREMIUM_MULTIPLIER); } if (product.isAdvertised()) { multipliers.add(ADVERTISED_MULTIPLIER); } if (product.getPrice().compareTo(EXPENSIVE_THRESHOLD) >= 0) { multipliers.add(EXPENSIVE_MULTIPLIER); } return multipliers; } 

يؤدي عدد كبير من الخيارات إلى حقيقة أن اثنين أو ثلاثة من اختبارات التكامل غير محدودة هنا. تعتبر مجموعة أضيق الحدود من اختبارات الوحدات مثالية لتصحيح هذه الوظائف.



يمكن العثور على مجموعة الاختبار المقابلة في فئة BonusPointCalculatorTest . هنا بعض منهم:

 @Test public void calculate_oneProduct() { Product product = product("product").price("1.00").build(); Customer customer = customer("customer").build(); Map<Product, Long> quantities = mapOf(product, 1L); BigDecimal bonus = bonusPointCalculator.calculate(customer, list(product), quantities::get); BigDecimal expectedBonus = bonusPoints("0.10").build(); assertEquals(expectedBonus, bonus); } @Test public void calculate_favProduct() { Product product = product("product").price("1.00").build(); Customer customer = customer("customer").favProduct(product).build(); Map<Product, Long> quantities = mapOf(product, 1L); BigDecimal bonus = bonusPointCalculator.calculate(customer, list(product), quantities::get); BigDecimal expectedBonus = bonusPoints("0.10").addMultiplier(FAVORITE_MULTIPLIER).build(); assertEquals(expectedBonus, bonus); } 

تجدر الإشارة إلى أنه في الاختبارات نشير على وجه التحديد إلى API العام للفئة - طريقة calculate . اختبار عقد فئة بدلاً من تنفيذه يتجنب انهيار الاختبارات بسبب التغييرات غير الوظيفية وإعادة الهيكلة.

أخيرًا ، عندما قمنا بفحص المنطق الداخلي باختبارات الوحدة ، لم نعد بحاجة إلى وضع كل هذه التفاصيل في التكامل. في هذه الحالة ، يكفي اختبار تمثيلي واحد أو أكثر ، على سبيل المثال هذا:

 @Test public void calculateBonusPoints_twoProductTypes_correctValueCalculated() { Product product1 = product("product1").price("1.01").build(); Product product2 = product("product2").price("10.00").build(); productRepository.save(product1); productRepository.save(product2); Customer customer = customer("customer").build(); customerRepository.save(customer); Map<Long, Long> quantities = mapOf(product1.getId(), 1L, product2.getId(), 2L); BigDecimal bonus = customerController.calculateBonusPoints( new CalculateBonusPointsRequest("customer", quantities) ); BigDecimal bonusPointsProduct1 = bonusPoints("0.10").build(); BigDecimal bonusPointsProduct2 = bonusPoints("1.00").quantity(2).build(); BigDecimal expectedBonus = bonusPointsProduct1.add(bonusPointsProduct2); assertEquals(expectedBonus, bonus); } 

كما هو الحال في اختبارات التكامل ، فإن مجموعة اختبارات الوحدة المستخدمة صغيرة جدًا ولا تضمن صحة التطبيق بالكامل. ومع ذلك ، فإن وجودها يزيد بشكل كبير من الثقة في التعليمات البرمجية ، ويسهل التصحيح ، ويعطي المكافآت الأخرى المدرجة في الجزء الأول من المقالة.

توصيات التنفيذ


آمل أن تكون الأقسام السابقة كافية لإقناع مطور واحد على الأقل لمحاولة بدء استخدام الاختبارات في مشروعهم. يسرد هذا الفصل بإيجاز التوصيات الرئيسية التي ستساعد على تجنب المشاكل الخطيرة وتؤدي إلى انخفاض تكاليف التنفيذ الأولية.

حاول البدء في تنفيذ الاختبارات على التطبيق الجديد. ستكون كتابة الاختبارات الأولى في مشروع إرث كبير أكثر صعوبة وتتطلب مهارة أكثر من تلك التي تم إنشاؤها حديثًا. لذلك ، إذا كان ذلك ممكنًا ، فمن الأفضل البدء بتطبيق جديد صغير. إذا لم يكن من المتوقع وجود تطبيقات كاملة جديدة ، فيمكنك محاولة تطوير بعض الأدوات المفيدة للاستخدام الداخلي. الشيء الرئيسي هو أن المهمة يجب أن تكون أكثر أو أقل واقعية - الأمثلة المبتكرة لن تعطي تجربة كاملة.

قم بإعداد اختبارات تجريبية منتظمة. إذا لم يتم تشغيل الاختبارات بشكل منتظم ، فعندئذ لا تتوقف عن أداء وظيفتها الرئيسية فقط - التحقق من صحة الرمز - ولكن أيضًا تصبح بسرعة قديمة. لذلك ، من المهم للغاية تكوين الحد الأدنى من خط أنابيب CI على الأقل مع التشغيل التلقائي للاختبارات في كل مرة يتم فيها تحديث الرمز في المستودع.

لا تطارد الغطاء. كما هو الحال في أي تقنية أخرى ، في البداية لن يتم الحصول على الاختبارات بأفضل جودة. يمكن أن تساعد الأدبيات ذات الصلة (الروابط في نهاية المقالة) أو المرشد المختص هنا ، ولكن هذا لا يلغي الحاجة إلى مخاريط الحشو الذاتي. تشبه الاختبارات في هذا الصدد بقية التعليمات البرمجية: لفهم كيف ستؤثر على المشروع ، يمكنك فقط بعد العيش معهم لفترة من الوقت. لذلك ، لتقليل الضرر ، من الأفضل ألا تلاحق الرقم والأرقام الجميلة مثل تغطية مائة بالمائة. بدلاً من ذلك ، يجب أن تقتصر على السيناريوهات الإيجابية الرئيسية لوظيفة التطبيق الخاص بك.

لا تفلت من اختبارات الوحدة. استمرارًا لموضوع "الكمية مقابل الجودة" ، تجدر الإشارة إلى أنه لا ينبغي إجراء اختبارات الوحدة النزيهة لأول مرة ، لأن ذلك يمكن أن يؤدي بسهولة إلى تحديد مفرط للتطبيق. في المقابل ، سيصبح هذا عاملاً مثبطًا خطيرًا في إعادة الهيكلة اللاحقة وتحسينات التطبيق. يجب استخدام اختبارات الوحدة فقط إذا كان هناك منطق معقد في فئة معينة أو مجموعة من الفئات ، وهو أمر غير ملائم للتحقق من مستوى التكامل.

لا تفلت من حصص كعب وطرق التطبيق. الأوتاد (كعب ، وهمية) هي أداة أخرى تتطلب نهجا متوازنا والحفاظ على التوازن. من ناحية ، يسمح لك العزل الكامل للوحدة بالتركيز على المنطق المختبر وعدم التفكير في بقية النظام. من ناحية أخرى ، سيتطلب هذا وقتًا إضافيًا للتطوير ، كما هو الحال مع اختبارات الوحدة ، يمكن أن يؤدي إلى تحديد مفرط للسلوك.

قم بفك اختبارات التكامل من الأنظمة الخارجية. من الأخطاء الشائعة في اختبارات التكامل استخدام قاعدة بيانات حقيقية وقوائم انتظار الرسائل وأنظمة أخرى خارج التطبيق. بالطبع ، تعد القدرة على إجراء اختبار في بيئة حقيقية مفيدة لتصحيح الأخطاء والتطوير. قد تكون هذه الاختبارات بكميات صغيرة منطقية ، خاصة بالنسبة للجري التفاعلي. ومع ذلك ، يؤدي استخدامها على نطاق واسع إلى عدد من المشاكل:

  1. لتشغيل الاختبارات ، ستحتاج إلى تكوين البيئة الخارجية. على سبيل المثال ، قم بتثبيت قاعدة بيانات على كل جهاز حيث سيتم تجميع التطبيق. سيؤدي ذلك إلى صعوبة دخول المطورين الجدد إلى المشروع وتكوين CI.
  2. قد تختلف حالة الأنظمة الخارجية على الأجهزة المختلفة قبل تشغيل الاختبارات. على سبيل المثال ، قد تحتوي قاعدة البيانات بالفعل على الجداول التي يحتاجها التطبيق مع البيانات غير المتوقعة في الاختبار. سيؤدي ذلك إلى حالات فشل غير متوقعة في الاختبارات ، وسيتطلب التخلص منها قدرًا كبيرًا من الوقت.
  3. إذا كان العمل الموازي جارًا في العديد من المشاريع ، فإن التأثير غير الواضح لبعض المشاريع على المشاريع الأخرى ممكن. على سبيل المثال ، يمكن أن تساعد إعدادات قاعدة بيانات محددة تم إعدادها لأحد المشاريع في عمل وظيفة مشروع آخر بشكل صحيح ، والتي ، مع ذلك ، ستتعطل عند إطلاقها في قاعدة بيانات نظيفة على جهاز آخر.
  4. يتم إجراء الاختبارات لفترة طويلة: يمكن أن يصل الجري الكامل إلى عشرات الدقائق. هذا يؤدي إلى حقيقة أن المطورين يتوقفون عن تشغيل الاختبارات محليًا ولا ينظرون إلى نتائجهم إلا بعد إرسال التغييرات إلى المستودع البعيد. هذا السلوك يبطل معظم مزايا الاختبارات التي نوقشت في الجزء الأول من المقالة.

مسح السياق بين اختبارات التكامل. في كثير من الأحيان ، لتسريع عمل اختبارات التكامل ، عليك إعادة استخدام نفس السياق بينهما. حتى وثائق الربيع الرسمية تقدم مثل هذه التوصية. في الوقت نفسه ، يجب تجنب تأثير الاختبارات على بعضها البعض. نظرًا لأنه يتم إطلاقها في ترتيب عشوائي ، فإن وجود مثل هذه الروابط يمكن أن يؤدي إلى أخطاء عشوائية لا يمكن إعادة إنتاجها. لمنع حدوث ذلك ، يجب ألا تترك الاختبارات أي تغييرات في السياق. على سبيل المثال ، عند استخدام قاعدة بيانات ، للعزل يكفي عادة استرجاع جميع المعاملات التي ارتكبت في الاختبار. إذا كان لا يمكن تجنب التغييرات في السياق ، يمكنك تكوين إعادة إنشائه باستخدام التعليق التوضيحي @DirtiesContext .

تأكد من إجراء الاختبارات في فترة زمنية معقولة. حتى إذا كانت الاختبارات مستقلة عن الأنظمة الخارجية الحقيقية ، فإن وقت تنفيذها يمكن أن يخرج عن السيطرة بسهولة. لمنع حدوث ذلك ، تحتاج إلى مراقبة هذا المؤشر باستمرار واتخاذ تدابير إذا لزم الأمر. أقل ما يمكن فعله هو فصل الاختبارات البطيئة إلى مجموعة منفصلة بحيث لا تتداخل مع العمل في المهام غير المتعلقة بها.

حاول أن تجعل الاختبارات واضحة ومقروءة قدر الإمكان. كما هو موضح بالفعل في المثال ، يجب كتابة الاختبارات بحيث لا تحتاج إلى فهمها. يمكن قضاء الوقت في تعلم الاختبار في تعلم الكود.

لا تعلق على TDD (التطوير القائم على الاختبار).TDD ممارسة شائعة إلى حد ما ، لكني لا أعتبرها إلزامية ، خاصة في المراحل الأولى من التنفيذ. بشكل عام ، لا ترتبط القدرة على كتابة اختبارات جيدة بنقطة كتابتها. ما هو مهم حقًا هو القيام بالتصحيح الأولي للرمز بالفعل في الاختبارات ، نظرًا لأن هذه إحدى الطرق الرئيسية لتوفير الوقت.

تتم كتابة الاختبارات الأولى ، ما هي الخطوة التالية؟


بعد ذلك ، تحتاج إلى مراقبة عمر الاختبارات في المشروع بعناية وطرح أسئلة دورية على نفسك مثل ما يلي:

  1. ما هي الاختبارات التي تتداخل مع إعادة الهيكلة والتحسينات (تتطلب تصحيحات مستمرة)؟ يجب إعادة كتابة هذه الاختبارات أو إزالتها بالكامل من المشروع واستبدالها بأخرى أعلى.
  2. ما الاختبارات التي تنقطع في الغالب وبشكل غير متوقع أثناء التشغيل المتعدد أو المتوازي ، عند التشغيل في بيئات مختلفة (كمبيوتر الزميل ، خادم CI)؟ كما أنها تتطلب معالجة.
  3. ما أخطاء اجتياز الاختبارات؟ لكل خطأ من هذا القبيل ، من المستحسن إضافة اختبار جديد ووضعها في الاعتبار عند كتابة الاختبارات لوظائف مماثلة.
  4. ما هي الاختبارات التي تستمر لفترة طويلة؟ يجب أن نحاول إعادة كتابتها. إذا لم يكن ذلك ممكنًا ، فقم بفصلها عن الأسرع من أجل الحفاظ على إمكانية التشغيل المحلي عبر الإنترنت.

بالإضافة إلى ذلك ، يجدر الانتباه إلى مزايا الاختبارات التي تم وصفها في بداية المقالة. إذا لم تستلمهم ، فهذا يعني أن هناك خطأ ما. يعد الاسترجاع المنتظم أرخص طريقة لزيادة جودة وفعالية الاختبارات المستخدمة باستمرار.

الخلاصة


في البداية ، من الأفضل عدم ملاحقة عدد الاختبارات ، ولكن التركيز على جودتها. يمكن أن يصبح عددًا كبيرًا من اختبارات الوحدة غير الملائمة مرساة ، مما يسحب المشروع إلى الأسفل. بالإضافة إلى ذلك ، لا يعفي وجود اختبارات الوحدة من الحاجة إلى كتابة اختبارات التكامل. لذلك ، فإن الإستراتيجية الأكثر فعالية لأول مرة هي البدء من خلال تغطية السيناريوهات الإيجابية الرئيسية باختبارات التكامل ، وإذا لم يكن ذلك كافيًا ، قم بإضافة فحوصات محلية مع اختبارات الوحدة. بمرور الوقت ، سوف تتراكم الملاحظات ، مما سيساعد على تصحيح الأخطاء التي ارتكبت والحصول على صورة أوضح للاستخدام الفعال لمختلف طرق الاختبار التلقائي.

آمل أنه من بين أولئك الذين يقرؤون سيكون هناك أولئك الذين ستتأثر خيوطهم الرفيعة من الروح بجرافيتي ، وستظهر العديد من المشاريع الأخرى ذات الاختبارات الجيدة والفعالة في العالم!

مصادر مشروع جيثب

أدب مفيد

Source: https://habr.com/ru/post/ar427603/


All Articles