ينطوي تطوير البرامج عالية الجودة على اختبار البرنامج وأجزائه. يتضمن اختبار الوحدة الكلاسيكية تقسيم برنامج كبير إلى كتل صغيرة ملائمة للاختبار. أو ، إذا تم تطوير الاختبارات بالتوازي مع تطوير الكود أو الاختبارات التي تم تطويرها قبل البرنامج (التطوير القائم على الاختبار - التطوير المدفوع باختبار) ، فإن البرنامج يتم تطويره في البداية في كتل صغيرة مناسبة لمتطلبات الاختبارات.
يمكن اعتبار أحد أصناف اختبار الوحدة اختبارًا قائمًا على الملكية (يتم تنفيذ هذا النهج ، على سبيل المثال ، في QuickCheck ، مكتبات ScalaCheck ). يعتمد هذا النهج على إيجاد خصائص عالمية يجب أن تكون صالحة لأي بيانات إدخال. على سبيل المثال ، يجب أن ينتج التسلسل متبوعًا بإلغاء التسلسل نفس الكائن . أو ، يجب ألا تؤدي إعادة الترتيب إلى تغيير ترتيب العناصر في القائمة . للتحقق من هذه الخصائص العالمية ، تدعم المكتبات المذكورة أعلاه آلية لتوليد بيانات إدخال عشوائية. يعمل هذا النهج بشكل جيد بشكل خاص للبرامج القائمة على القوانين الرياضية التي تعمل كخصائص عالمية صالحة لفئة واسعة من البرامج. حتى أن هناك مكتبة من الخصائص الرياضية الجاهزة - الانضباط - والتي تسمح لك بالتحقق من أداء هذه الخصائص في البرامج الجديدة (مثال جيد على إعادة استخدام الاختبارات).
في بعض الأحيان اتضح أنه من الضروري اختبار برنامج معقد دون القدرة على تحليله إلى أجزاء يمكن التحقق منها بشكل مستقل. في هذه الحالة ، يكون برنامج الاختبار أسود الصندوق الأبيض (أبيض - لأن لدينا الفرصة لدراسة الهيكل الداخلي للبرنامج).
تحت القطع ، تم وصف العديد من الأساليب لاختبار البرامج المعقدة مع إدخال واحد بدرجات متفاوتة من التعقيد (المشاركة) ودرجات متفاوتة من التغطية.
* في هذه المقالة ، نفترض أنه يمكن تمثيل البرنامج قيد الاختبار كوظيفة نقية دون حالة داخلية. (يمكن تطبيق بعض الاعتبارات أدناه إذا كانت الحالة الداخلية موجودة ، ولكن من الممكن إعادة تعيين هذه الحالة إلى قيمة ثابتة.)
اختبار مقاعد البدلاء
بادئ ذي بدء ، نظرًا لأنه يتم اختبار وظيفة واحدة فقط ، يكون رمز الاتصال هو نفسه دائمًا ، لا نحتاج إلى إنشاء اختبارات وحدة منفصلة. ستكون جميع هذه الاختبارات هي نفسها ، ودقيقة للمدخلات والشيكات. يكفي نقل البيانات المصدر ( input
) في حلقة والتحقق من النتائج ( expectedOutput
). من أجل تحديد مجموعة مشكلة من بيانات الاختبار في حالة الكشف عن الخطأ ، يجب وضع علامة على جميع بيانات الاختبار. وبالتالي ، يمكن تمثيل مجموعة واحدة من بيانات الاختبار على أنها ثلاثية:
case class TestCase[A, B](label: String, input: A, expectedOutput: B)
يمكن تمثيل نتيجة تشغيل واحد كـ TestCaseResult
:
case class TestCaseResult[A, B](testCase: TestCase[A, B], actualOutput: Try[B])
(نقدم نتيجة الإطلاق باستخدام Try
التقاط الاستثناءات المحتملة.)
لتبسيط تشغيل جميع بيانات الاختبار من خلال البرنامج قيد الاختبار ، يمكنك استخدام وظيفة المساعد التي ستستدعي البرنامج لكل قيمة إدخال:
def runTestCases[A, B](cases: Seq[TestCase[A, B])(f: A => B): Seq[TestCaseResult[A, B]] = cases .map{ testCase => TestCaseResult(testCase, Try{ f(testCase.input) } ) } .filter(r => r.actualOutput != Success(r.testCase.expectedOutput))
ستعيد وظيفة المساعد هذه البيانات والنتائج الإشكالية التي تختلف عن المتوقع.
للراحة ، يمكنك تنسيق نتائج الاختبار.
def report(results: Seq[TestCaseResult[_, _]]): String = s"Failed ${results.length}:\n" + results .map(r => r.testCase.label + ": expected " + r.testCase.expectedOutput + ", but got " + r.actualOutput) .mkString("\n")
وعرض تقرير فقط في حالة وجود أخطاء:
val testCases = Seq( TestCase("1", 0, 0) ) test("all test cases"){ val testBench = runTestCases(testCases) _ val results = testBench(f) assert(results.isEmpty, report(results)) }
إعداد المدخلات
في أبسط الحالات ، يمكنك إنشاء بيانات اختبار يدويًا لاختبار البرنامج وكتابته مباشرة في رمز الاختبار واستخدامه ، كما هو موضح أعلاه. غالبًا ما يتبين أن الحالات المثيرة للاهتمام لبيانات الاختبار تشترك كثيرًا ويمكن تقديمها على أنها بعض الأمثلة الأساسية ، مع تغييرات طفيفة.
val baseline = MyObject(...)
عند العمل باستخدام تراكيب البيانات الثابتة غير المتداخلة ، تساعد العدسات بشكل كبير ، على سبيل المثال ، من مكتبة Monocle :
val baseline = ??? val testObject1 = (field1 composeLens field2).set("123")(baseline)
تتيح لك العدسات "تعديل" الأجزاء المتداخلة بعمق في هياكل البيانات بأناقة: كل عدسة هي عبارة عن أداة ضبط وجلب لخاصية واحدة. يمكن دمج العدسات لإنتاج عدسات "تركز" على المستوى التالي.
استخدام DSL لتقديم التغييرات
بعد ذلك ، سننظر في تكوين بيانات الاختبار عن طريق إجراء تغييرات على كائن إدخال أولي. عادة ، للحصول على كائن الاختبار الذي نحتاجه ، نحتاج إلى إجراء بعض التغييرات. في هذه الحالة ، من المفيد جدًا تضمين قائمة بالتغييرات في الوصف النصي لـ TestCase:
val testCases = Seq( TestCase("baseline", baseline, ???), TestCase("baseline + " + "(field1 = 123) + " +
ثم سنعرف دائمًا عن بيانات الاختبار التي يتم إجراؤها.
حتى لا تختلف القائمة النصية للتغييرات عن التغييرات الفعلية ، يجب عليك اتباع مبدأ "نسخة واحدة من الحقيقة". (إذا كانت نفس المعلومات مطلوبة / مستخدمة في عدة نقاط ، فيجب أن يكون هناك مصدر أساسي وحيد للمعلومات الفريدة ، ويجب توزيع المعلومات على جميع نقاط الاستخدام الأخرى تلقائيًا ، مع التحولات اللازمة. إذا تم انتهاك هذا المبدأ وكان النسخ اليدوي للمعلومات أمرًا لا مفر منه . معلومات الإصدار التناقض في نقاط مختلفة وبعبارة أخرى في وصف بيانات الاختبار، ونحن نرى واحدة، وبيانات الاختبار - مثال آخر، نسخ التغيير field2 = "456"
وتعديله في field3 = "789"
نحن Mauger قصد ننسى لتصحيح الوصف. ونتيجة لذلك، فإن وصف تعكس سوى اثنين من التغييرات من ثلاثة).
في حالتنا ، فإن المصدر الأساسي للمعلومات هو التغييرات نفسها ، أو بالأحرى ، الكود المصدر للبرنامج الذي يقوم بإجراء التغييرات. نود أن نستنتج منهم نصًا يصف التغييرات. مرتجلاً ، كخيار أول ، يمكنك اقتراح استخدام ماكرو لالتقاط التعليمات البرمجية المصدر للتغييرات ، واستخدام التعليمات البرمجية المصدر كمستندات. هذه ، على ما يبدو ، طريقة جيدة وغير معقدة نسبيًا لتوثيق التغييرات الفعلية وقد يتم تطبيقها في بعض الحالات. لسوء الحظ ، إذا قدمنا التغييرات في النص العادي ، فإننا نفقد القدرة على إجراء تحويلات ذات معنى لقائمة التغييرات. على سبيل المثال ، اكتشف وقم بإزالة التغييرات المكررة أو المتداخلة ، وقم بوضع قائمة بالتغييرات بطريقة مناسبة للمستخدم النهائي.
لتتمكن من التعامل مع التغييرات ، يجب أن يكون لديك نموذج منظم لها. يجب أن يكون النموذج معبرًا بما يكفي لوصف جميع التغييرات التي تهمنا. جزء من هذا النموذج ، على سبيل المثال ، سيكون عنونة حقول الكائن والثوابت وعمليات التخصيص.
يجب أن يسمح نموذج التغيير بحل المهام التالية:
- حالات نموذج تفرخ التغيير. (أي إنشاء قائمة محددة بالتغييرات بالفعل.)
- تشكيل وصف نصي للتغييرات.
- تطبيق التغييرات على كائنات المجال.
- إجراء تحويلات التحسين على النموذج.
إذا تم استخدام لغة برمجة عالمية لإجراء تغييرات ، فقد يكون من الصعب تمثيل هذه التغييرات في النموذج. يمكن أن يستخدم الكود المصدري للبرنامج تركيبات معقدة لا يدعمها النموذج. يمكن لمثل هذا البرنامج استخدام أنماط ثانوية ، مثل العدسات أو طريقة copy
، لتغيير حقول كائن ما ، وهي تجريدات ذات مستوى أدنى بالنسبة لمستوى نموذج التغيير. ونتيجة لذلك ، قد تكون هناك حاجة إلى تحليل إضافي لهذه الأنماط لإخراج حالات من التغييرات. وبالتالي ، فإن الخيار الجيد في البداية باستخدام الماكرو ليس مناسبًا للغاية.
طريقة أخرى لإنشاء مثيلات نموذج التغيير يمكن أن تكون لغة متخصصة (DSL) ، والتي تنشئ كائنات نموذج التغيير باستخدام مجموعة من طرق الامتداد والعوامل المساعدة. حسنًا ، في أبسط الحالات ، يمكن إنشاء مثيلات نموذج التغيير مباشرة من خلال المنشئين.
تغيير تفاصيل اللغةلغة التغيير هي بنية معقدة نوعًا ما تحتوي على العديد من المكونات ، والتي بدورها غير بديهية.
- نموذج هيكل البيانات.
- تغيير النموذج.
- تضمين فعليًا (؟) DSL - الإنشاءات الإضافية ، طرق التمديد ، لبناء مريح للتغيرات.
- مترجم التغييرات التي تسمح لك في الواقع "بتعديل" كائن (في الواقع ، بالطبع ، إنشاء نسخة معدلة).
فيما يلي مثال لبرنامج مكتوب باستخدام DSL:
val target: Entity[Target]
أي ، باستخدام طرق التمديد \
و :=
، يتم تكوين كائنات PropertyAccess
، SetProperty
كائنات target
تم إنشاؤها مسبقًا ، field1
، subobject
، field2
. أيضًا ، نظرًا للتحويلات الضمنية (الخطيرة) ، يتم LiftedString
السلسلة "123" في LiftedString
(يمكنك الاستغناء عن التحويلات الضمنية واستدعاء الطريقة المقابلة بشكل صريح: lift("123")
).
يمكن استخدام علم الوجود المكتوب كنموذج بيانات (راجع https://habr.com/post/229035/ و https://habr.com/post/222553/ ). (باختصار: يتم الإعلان عن كائنات الأسماء التي تمثل خصائص أي نوع من المجال: val field1: Property[Target, String]
.) في هذه الحالة ، يمكن تخزين البيانات نفسها ، على سبيل المثال ، في شكل JSON. تكمن راحة علم الوجود المكتوب في حالتنا في حقيقة أن نموذج التغيير يعمل عادة مع الخصائص الفردية للكائنات ، ويوفر علم الوجود فقط أداة مناسبة لمعالجة الخصائص.
لتمثيل التغييرات ، تحتاج إلى مجموعة من الفئات من نفس الخطة مثل فئة SetProperty
أعلاه:
Modify
- تطبيق الوظيفة ،Changes
- تطبيق تغييرات متعددة بالتتابعForEach
- تطبيق التغييرات على كل عنصر في المجموعة ،- الخ.
مترجم لغة التغيير هو مقيِّم تعبير تعاودي منتظم يعتمد على PatternMatching. شيء مثل:
def eval(expression: DslExpression, gamma: Map[String, Any]): Any = expression match { case LiftedString(str) => str case PropertyAccess(obj, prop) => Getter(prop)(gamma).get(obj) } def change[T] (expression: DslChangeExpression, gamma: Map[String, Any], target: T): T = expression match { case SetProperty(path, valueExpr) => val value = eval(valueExpr, gamma) Setter(path)(gamma).set(value)(target) }
للعمل مباشرة على خصائص الكائنات ، يجب عليك تحديد getter و setter لكل خاصية مستخدمة في نموذج التغيير. يمكن تحقيق ذلك عن طريق ملء الخريطة بين الخصائص الوجودية وعدساتها المقابلة.
هذا النهج ككل يعمل ، ويسمح لك بالفعل بوصف التغييرات مرة واحدة ، ولكن هناك حاجة تدريجية لتمثيل المزيد والمزيد من التغييرات المعقدة ونموذج التغييرات ينمو إلى حد ما. على سبيل المثال ، إذا كنت بحاجة إلى تغيير خاصية باستخدام قيمة خاصية أخرى لنفس الكائن (على سبيل المثال ، field1 = field2 + 1
) ، فأنت بحاجة إلى دعم المتغيرات على مستوى DSL. وإذا كان تغيير الخاصية أمرًا بسيطًا ، فمن الضروري دعم التعبيرات الحسابية والوظائف على مستوى DSL.
اختبار الفروع
يمكن أن يكون رمز الاختبار خطيًا ، وبعد ذلك ، بشكل عام ، يكفي مجموعة واحدة من بيانات الاختبار لفهم ما إذا كان يعمل. إذا كان هناك فرع ( if-then-else
) ، يجب عليك تشغيل المربع الأبيض مرتين على الأقل ببيانات إدخال مختلفة بحيث يتم تنفيذ كلا الفرعين. يبدو أن عدد مجموعات بيانات الإدخال الكافية لتغطية جميع الفروع يساوي عدديًا التعقيد الحلقي للكود مع الفروع.
كيف تشكل جميع مجموعات بيانات الإدخال؟ نظرًا لأننا نتعامل مع مربع أبيض ، يمكننا عزل شروط التفرع وتعديل كائن الإدخال مرتين بحيث يتم تنفيذ فرع واحد في حالة واحدة ، وفي حالة أخرى أخرى. فكر في مثال:
if (object.field1 == "123") A else B
بوجود مثل هذه الحالة ، يمكننا تشكيل حالتين اختبار:
val testCase1 = TestCase("A", field1.set("123")(baseline), ) val testCase2 = TestCase("B", field1.set("123" + "1">)(baseline), )
(في حالة عدم إمكانية إنشاء أحد سيناريوهات الاختبار ، يمكننا عندئذ أن نفترض أنه تم الكشف عن رمز خامد ، ويمكن إزالة الحالة ، جنبًا إلى جنب مع الفرع المقابل ، بأمان.)
إذا تم فحص الخصائص المستقلة للكائن في عدة فروع ، فمن السهل جدًا تكوين مجموعة شاملة من كائنات الاختبار المعدلة التي تغطي جميع التركيبات الممكنة تمامًا.
DSL لتشكيل جميع تركيبات التغييراتدعونا نفكر بمزيد من التفصيل في الآلية التي تسمح بتشكيل جميع قوائم التغييرات الممكنة التي توفر تغطية كاملة لجميع الفروع. من أجل استخدام قائمة التغييرات أثناء الاختبار ، نحتاج إلى دمج جميع التغييرات في كائن واحد ، والذي سنقدمه لإدخال الشفرة المختبرة ، أي أن دعم التكوين مطلوب. للقيام بذلك ، يمكنك إما استخدام DSL أعلاه لنمذجة التغييرات ، وبعد ذلك تكفي قائمة بسيطة بالتغييرات ، أو يمكنك تقديم تغيير واحد كوظيفة تعديل T => T
:
val change1: T => T = field1.set("123")(_)
عندها ستكون سلسلة التغييرات ببساطة عبارة عن تكوين للوظائف:
val changes = change1 compose change2
أو للحصول على قائمة بالتغييرات:
val rawChangesList: Seq[T => T] = Seq(change1, change2) val allChanges: T => T = rawChangesList.foldLeft(identity)(_ compose _)
لتسجيل جميع التغييرات المطابقة لجميع الفروع الممكنة ، يمكنك استخدام DSL للمستوى التالي من التجريد ، والذي يحاكي بنية المربع الأبيض المختبر:
val tests: Seq[(String, T => T)] = IF("field1 == '123'")
تحتوي مجموعة tests
هنا على تغييرات مجمعة تتوافق مع جميع المجموعات الممكنة من الفروع. ستحتوي معلمة من النوع String
على جميع أسماء الشروط وجميع أوصاف التغييرات التي يتم من خلالها تكوين دالة التغيير المجمعة. والعنصر الثاني لزوج من النوع T => T
هو مجرد وظيفة مجمعة للتغيرات التي تم الحصول عليها نتيجة لتكوين التغييرات الفردية.
للحصول على الكائنات التي تم تغييرها ، تحتاج إلى تطبيق جميع وظائف التغيير المجمعة على كائن الأساس:
val tests2: Seq[(String, T)] = tests.map(_.map_2(_(baseline)))
ونتيجة لذلك ، نحصل على مجموعة من الأزواج ، وسيصف الخط التغييرات المطبقة ، وسيكون العنصر الثاني في الزوج هو الكائن الذي يتم فيه دمج كل هذه التغييرات.
استنادًا إلى هيكل نموذج الشفرة المختبرة في شكل شجرة ، ستمثل قوائم التغييرات المسار من الجذر إلى أوراق هذه الشجرة. وبالتالي ، سيتم تكرار جزء كبير من التغييرات. يمكنك التخلص من هذه الازدواجية باستخدام خيار DSL ، حيث يتم تطبيق التغييرات مباشرة على كائن الخط الأساسي أثناء التنقل عبر الفروع. في هذه الحالة ، سيتم إجراء عدد أقل من الحسابات غير الضرورية.
بما أننا نتعامل مع صندوق أبيض ، يمكننا أن نرى جميع الفروع. هذا يجعل من الممكن بناء نموذج للمنطق الموجود في مربع أبيض واستخدام النموذج لإنشاء بيانات الاختبار. إذا كان رمز الاختبار مكتوبًا في Scala ، فيمكنك ، على سبيل المثال ، استخدام scalameta لقراءة الرمز ، مع التحويل اللاحق إلى نموذج منطقي. مرة أخرى ، كما في المسألة التي نوقشت سابقًا في نمذجة منطق التغييرات ، من الصعب بالنسبة لنا أن نمثل كل احتمالات اللغة العالمية. علاوة على ذلك ، سنفترض أن الشفرة المختبرة يتم تنفيذها باستخدام مجموعة فرعية محدودة من اللغة ، أو بلغة أخرى أو DSL ، وهي محدودة في البداية. هذا يسمح لنا بالتركيز على تلك الجوانب من اللغة التي تهمنا.
فكر في مثال على رمز يحتوي على فرع واحد:
if(object.field1 == "123") A else B
يقسم الشرط مجموعة قيم field1
إلى فئتين == "123"
: == "123"
و != "123"
. وبالتالي ، تنقسم المجموعة الكاملة لبيانات الإدخال أيضًا إلى فئتين ClassCondition1IsTrue
فيما يتعلق بهذا الشرط - ClassCondition1IsTrue
و ClassCondition1IsFalse
. من وجهة نظر اكتمال التغطية ، يكفي أن نأخذ مثالًا واحدًا على الأقل من هاتين الفئتين لتغطية الفرعين A
و B
بالنسبة للفئة الأولى ، يمكننا بناء مثال ، بمعنى ما ، بطريقة فريدة: خذ كائنًا عشوائيًا ، ولكن غير field1
إلى "123"
. علاوة على ذلك ، سيظهر الكائن بالتأكيد في فئة التكافؤ ClassCondition1IsTrue
الحسابات على طول الفرع A
هناك المزيد من الأمثلة للفئة الثانية. تتمثل إحدى طرق إنشاء بعض الأمثلة على الفئة الثانية في إنشاء كائنات إدخال عشوائية وتجاهل تلك التي تحتوي على field1 == "123"
. طريقة أخرى: لأخذ كائن عشوائي ، ولكن تغيير field1
إلى "123" + "*"
(للتعديل ، يمكنك استخدام أي تغيير في خط التحكم للتأكد من أن الخط الجديد لا يساوي خط التحكم).
Arbitrary
Gen
من مكتبة ScalaCheck مناسبة تمامًا Arbitrary
بيانات عشوائية.
بشكل أساسي ، نطلق على الدالة المنطقية المستخدمة في if
. أي أننا نجد جميع قيم كائن الإدخال التي تأخذ فيها هذه الدالة المنطقية القيمة true
- ClassCondition1IsTrue
، وجميع قيم كائن الإدخال التي تأخذ القيمة false
- ClassCondition1IsFalse
.
بطريقة مماثلة ، من الممكن توليد بيانات مناسبة للقيود الناتجة عن عوامل شرطية بسيطة ذات ثوابت (أكثر / أقل من ثابت ، مضمّن في مجموعة ، يبدأ بثابت). من السهل عكس هذه الظروف. حتى إذا تم استدعاء وظائف بسيطة في رمز الاختبار ، يمكننا استبدال مكالمتهم بتعريفها (مضمن) واستمرار عكس التعبيرات الشرطية.
وظائف عكسية صعبة
يختلف الموقف عندما تستخدم الحالة دالة يصعب عكسها. على سبيل المثال ، إذا تم استخدام دالة التجزئة ، فلن يكون من الممكن إنشاء مثال تلقائيًا لإعطاء القيمة المطلوبة لرمز التجزئة.
في هذه الحالة ، يمكنك إضافة معلمة إضافية إلى كائن الإدخال الذي يمثل نتيجة حساب الوظيفة ، واستبدال استدعاء الوظيفة باستدعاء لهذه المعلمة ، وتحديث هذه المعلمة ، على الرغم من انتهاك الاتصال الوظيفي:
if(sha(object.field1)=="a9403...") ... // ==> if(object.sha_field1 == "a9403...") ...
تسمح المعلمة الإضافية بتنفيذ التعليمات البرمجية داخل الفرع ، ولكن من الواضح أنها يمكن أن تؤدي إلى نتائج غير صحيحة في الواقع. أي أن برنامج الاختبار سيحقق نتائج لا يمكن ملاحظتها في الواقع. ومع ذلك ، فإن التحقق من جزء من الكود لا يمكن الوصول إلينا به بخلاف ذلك لا يزال مفيدًا ويمكن اعتباره شكلاً من أشكال اختبار الوحدة. بعد كل شيء ، حتى أثناء اختبار الوحدة ، يتم استدعاء الوظيفة الفرعية بحجج قد لا يتم استخدامها مطلقًا في البرنامج.
بمثل هذه التلاعبات ، نقوم باستبدال (استبدال) كائن الاختبار. ومع ذلك ، إلى حد ما ، يتضمن البرنامج الذي تم إنشاؤه حديثًا منطق البرنامج القديم. في الواقع ، إذا أخذنا كقيم المعلمات الاصطناعية الجديدة نتائج حساب الوظائف التي استبدلناها بالمعلمات ، فسينتج البرنامج نفس النتائج. يبدو أن اختبار البرنامج المعدل قد يكون ذا فائدة. ما عليك سوى أن تتذكر الظروف التي سيعمل فيها البرنامج الذي تم تغييره مثل البرنامج الأصلي.
الظروف التابعة
, . , , , . , . (, , x > 0
, — x <= 1
. , — (-∞, 0]
, (0, 1]
, (1, +∞)
, — .)
, , , true
false
. , , " " .
, , :
if(x > 0) if(y > 0) if (y > x)
( > 0
, — y > x
.)
"", , , , , . , " " .
, "", ( y == x + 1
), , .
"" ( y > x + 1 && y < x + 2
), , .
, , - , "c " ( Symbolic Execution , ), . ( field1 = field1_initial_value
). , . :
val a = field1 + 10
— true
false
. . . على سبيل المثال
if(a > 0) A else B
, , , , . , (, , ).
. , , . , . , .
, . . , , . , , , . , , , , , ?
Y- ( " " , stackoverflow:What is a Y-combinator? (2- ) , habr: Y- 7 ). , . ( , , .) . , . , "" . Y- " " ( ).
( ). , . , . , . , , , TestCase
'. , , ( throw
Nothing
bottom
, ). .
, . . , , . , . , , . , , , , . , . , .
, , , , 100% . , , . جلالة. . , , , ? , , - .
:
- .
- ( ).
- , .
- , .
, , . -, , , , . -, , , ( , ), , , "" . / , .
الخلاصة
" " " ". , , , , . , .
, , , , . -, , ( ), . -, -, . DSL, , . -, , . -, , ( , , ). .
, , . , , - .
شكر وتقدير
@mneychev .