الوحدة السريعة أو النهج التصريحي لاختبارات الوحدة



مرحبا! اسمي يوري سكفورتسوف ، يشارك فريقنا في الاختبار الآلي في Rosbank. تتمثل إحدى مهامنا في تطوير أدوات لأتمتة الاختبارات الوظيفية.



في هذه المقالة ، أريد أن أتحدث عن حل تم تصوره على أنه أداة مساعدة صغيرة لحل المشكلات الأخرى ، لكنه تحول في النهاية إلى أداة مستقلة. نحن نتحدث عن إطار عمل الوحدة السريعة ، والذي يسمح لك بكتابة اختبارات الوحدة بأسلوب تصريحي وتحويل تطوير اختبارات الوحدة إلى مُنشئ مكون. تم تطوير المشروع بشكل أساسي لاختبار منتجنا الرئيسي - Tladianta - وهو إطار عمل BDD موحد لاختبار 4 منصات: سطح المكتب والويب والجوال والباقي.



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



في المرحلة الأولى ، حاولنا استخدام أدوات جاهزة مثل assertJ و Mockito ، لكن سرعان ما واجهنا بعض الميزات التقنية لمشروعنا:



  • تستخدم Tladianta بالفعل JUnit4 كعنصر تبعية ، مما يجعل من الصعب استخدام إصدار مختلف من JUnit ويجعل من الصعب العمل مع Before ؛
  • يحتوي Tladianta على مكونات للعمل مع منصات مختلفة ، ولديه العديد من الكيانات "القريبة للغاية" من حيث الوظائف ، ولكن مع التسلسلات الهرمية المختلفة والسلوكيات المختلفة ؛
  • «» ( ) ;
  • , , , , ;
  • - (, Appium , , , );
  • , : Mockito .




في البداية ، عندما تعلمنا للتو كيفية استبدال السائق ، وإنشاء عناصر زائفة من السيلينيوم وكتابة البنية الأساسية لحزمة الاختبار ، بدت الاختبارات شيئًا كالتالي:



@Test
public void checkOpenHint() {
    ElementManager.getInstance().register(xpath,ElementManager.Condition.VISIBLE,
ElementManager.Condition.DISABLED);
    new HintStepDefs().open(("");
    assertTrue(TestResults.getInstance().isSuccessful("Open"));
    assertTrue(TestResults.getInstance().isSuccessful("Click"));
}

@Test
public void checkCloseHint() {
    ElementManager.getInstance().register(xpath);
    new HintStepDefs().close("");
    assertTrue(TestResults.getInstance().isSuccessful("Close"));
    assertTrue(TestResults.getInstance().isSuccessful("Click"));
}


أو حتى مثل هذا:



@Test
public void fillFieldsTestOld() {
    ElementManager.getInstance().register(ElementManager.Type.CHECK_BOX,"//check-box","",
ElementManager.Condition.NOT_SELECTED);
        ElementManager.getInstance().register(ElementManager.Type.INPUT,"//input","");
        ElementManager.getInstance().register(ElementManager.Type.RADIO_GROUP, 
"//radio-group","");
        DataTable dataTable = new Cucumber.DataTableBuilder()
                .withRow("", "true")
                .withRow("", "not selected element")
                .withRow(" ", "text")
                .build();
        new HtmlCommonSteps().fillFields(dataTable);
        assertEquals(TestResults.getInstance().getTestResult("set"), 
ElementProvider.getInstance().provide("//check-box").force().getAttribute("test-id"));
        assertEqualsTestResults.getInstance().getTestResult("sendKeys"), 
ElementProvider.getInstance().provide("//input").force().getAttribute("test-id"));
        assertEquals(TestResults.getInstance().getTestResult("selectByValue"), 
ElementProvider.getInstance().provide("//radio-group").force().getAttribute("test-id"));
    }


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



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



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



تغيير الفلسفة



إذا نظرت إلى الكود ككل ، يمكنك أن ترى أن العديد من كتل التعليمات البرمجية تتكرر "بدون معنى". نقوم باختبار الطرق ، لكننا نستخدم المُنشئين طوال الوقت (لتجنب إمكانية تخزين بعض الأخطاء مؤقتًا). التحول الأول - نقلنا عمليات التحقق وإنشاء المثيلات المختبرة إلى التعليقات التوضيحية.



@IExpectTestResult(errDesc = "    set", value = "set",
expected = "//check-box", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = "    sendKeys", value = "sendKeys", 
expected = "//input", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = "    selectByValue", value = "selectByValue",
expected = "//radio-group", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@Test
public void fillFieldsTestOld() {
    ElementManager.getInstance().register(ElementManager.Type.CHECK_BOX, "//check-box", "",
ElementManager.Condition.NOT_SELECTED);
    ElementManager.getInstance().register(ElementManager.Type.INPUT, "//input", "");
    ElementManager.getInstance().register(ElementManager.Type.RADIO_GROUP, 
"//radio-group", "");
    DataTable dataTable = new Cucumber.DataTableBuilder()
            .withRow("", "true")
            .withRow("", "not selected element")
            .withRow(" ", "text")
            .build();
    runTest("fillFields", dataTable);
}


ما الذي تغير؟



  • تم تفويض عمليات التحقق إلى مكون منفصل. الآن لست بحاجة إلى معرفة كيفية تخزين العناصر ونتائج الاختبار.
  • : errDesc , .
  • , , , – runTest, , .
  • .
  • - , .


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



@IGenerateElement(type = ElementManager.Type.CHECK_BOX)
@IGenerateElement(type = ElementManager.Type.RADIO_GROUP)
@IGenerateElement(type = ElementManager.Type.INPUT)
@Test
@IExpectTestResult(errDesc = "    set", value = "set", 
expected = "//check-box", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = "    sendKeys", value = "sendKeys", 
expected = "//input", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = "    selectByValue", value = "selectByValue",
expected = "//radio-group", convertedBy = Converters.XpathToIdConverter.class, soft = true)
public void fillFieldsTest() {
    DataTable dataTable = new Cucumber.DataTableBuilder()
            .withRow("", "true")
            .withRow("", "not selected element")
            .withRow(" ", "text")
            .build();
    runTest("fillFields", dataTable);
}


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



نواصل التطوير



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



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


يتم وصف التعليقات التوضيحية المطلوب معالجتها على النحو التالي:



@Target(ElementType.PACKAGE) //  
@IPhase(value = "package-generate", processingClass = IStabDriver.StabDriverProcessor.class,
priority = 1) //    (      )
public @interface IStabDriver {

    Class<? extends WebDriver> value(); //   ,     

    class StabDriverProcessor implements PhaseProcessor<IStabDriver> { // 
        @Override
        public void process(IStabDriver iStabDriver) {
            //  
        }
    }
}


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



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



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



المعلمات الافتراضية:



@IProvideInstance
CheckBox generateCheckBox() {
    return new CheckBox((MobileElement) ElementProvider.getInstance().provide("//check-box")
.get());
}


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



الحقن التلقائي للوسيطة: لنفترض أن لدينا مُنشئًا مثل هذا:



public Mask(String dataFormat, String fieldFormat) {
    this.dataFormat = dataFormat;
    this.fieldFormat = fieldFormat;
}


ثم سيبدو اختبار هذه الفئة باستخدام حقن الوسيطة كما يلي:



Object[] dataMask={"_:2_:2_:4","_:2/_:2/_:4"};

@ITestInstance(argSource = "dataMask")
@Test
@IExpectTestResult(errDesc = "  ", value = FAST_RESULT,
expected = "12/10/2012")
public void convert() {
    runTest("convert","12102012");
}


مقدمي الأسماء



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



@IProvideInstance("")
Mask createDataMask(){
    return new Mask("_:2_:2_:4","_:2/_:2/_:4");
} 

@ITestInstance("")
@Test
@IExpectTestResult(errDesc = "  ", value = FAST_RESULT,
expected = "12/10/2012")
public void convert() {
    runTest("convert","12102012");
}


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



الاستنتاجات والنتائج



اتضح أن نهجنا يتمتع بالعديد من المزايا:



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


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



المشاكل والخطط الحالية:



  • , . , ( - ).
  • .
  • .
  • , -.
  • Fast-Unit junit4, junit5 testng



All Articles