Unit Fairy Magic Fairy: DSL in C #

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


في مدرسة مطور Dodo DevSchool ، نلقي الضوء ، من بين أمور أخرى ، على المعايير التالية لاختبار جيد:

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

كيف تحب مثل هذا الاختبار من حيث هذه المعايير؟

[Fact] public void AcceptOrder_Successful() { var ingredient1 = new Ingredient("Ingredient1"); var ingredient2 = new Ingredient("Ingredient2"); var ingredient3 = new Ingredient("Ingredient3"); var order = new Order(DateTime.Now); var product1 = new Product("Pizza1"); product1.AddIngredient(ingredient1); product1.AddIngredient(ingredient2); var orderLine1 = new OrderLine(product1, 1, 500); order.AddLine(orderLine1); var product2 = new Product("Pizza2"); product2.AddIngredient(ingredient1); product2.AddIngredient(ingredient3); var orderLine2 = new OrderLine(product2, 1, 650); order.AddLine(orderLine2); var orderRepositoryMock = new Mock<IOrderRepository>(); var ingredientsRepositoryMock = new Mock<IIngredientRepository>(); var service = new PizzeriaService(orderRepositoryMock.Object, ingredientsRepositoryMock.Object); service.AcceptOrder(order); orderRepositoryMock.Verify(r => r.Add(order), Times.Once); ingredientsRepositoryMock.Verify(r => r.ReserveIngredients(order), Times.Once); } 

بالنسبة لي - سيئة للغاية.

إنه أمر غير مفهوم: على سبيل المثال ، لا يمكنني حتى تخصيص مجموعات الترتيب والتصرف والتأكيد.

غير قابل للتشغيل: يتم استخدام الخاصية DateTime.Now. وأخيرا ، هو غير مركّز ، لأن له سببان للسقوط: يتم فحص الاستدعاءات لطرق مستودعين.

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

  1. يتم إنشاء المكونات.
  2. من المكونات ، يتم إنشاء منتجات (البيتزا).
  3. يتم إنشاء طلب من المنتجات.
  4. يتم إنشاء خدمة تكون مستودعاتها رطبة.
  5. يتم تمرير الطلب إلى أسلوب AcceptOrder للخدمة.
  6. تم التحقق من أنه قد تم استدعاء أساليب Add و ReserveIngredients الخاصة بمستودعات التخزين المعنية.

فكيف نجعل هذا الاختبار أفضل؟ تحتاج إلى محاولة مغادرة نص الاختبار فقط المهم. ولهذا السبب ، توصل الأشخاص الأذكياء مثل Martin Fowler و Rebecca Parsons إلى DSL (لغة نطاق محددة) . سأتحدث هنا عن أنماط DSL التي نستخدمها في Dodo لضمان أن تكون اختبارات وحدتنا ناعمة وحريرية ، ويشعر المطورون بالثقة كل يوم.

الخطة هي: أولاً سنجعل هذا الاختبار مفهومًا ، ثم سنعمل على التكاثر وينتهي الأمر بجعله مركزًا. سافرنا ...

التخلص من المكونات (كائنات المجال المحددة مسبقًا)


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

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

 public static class Ingredients { public static readonly Ingredient Dough = new Ingredient("Dough"); public static readonly Ingredient Pepperoni = new Ingredient("Pepperoni"); public static readonly Ingredient Mozzarella = new Ingredient("Mozzarella"); } 

فبدلاً من Ingredient1 و Ingredient2 و Ingredient3 المجنون تمامًا ، حصلنا على عجين وبيبروني وموزاريلا.
استخدم كائنات المجال المعرفة مسبقًا لكيانات المجال شائعة الاستخدام.

باني للمنتجات


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

هنا ، نمط البناء القديم الجيد يأتي في متناول يدي. فيما يلي إصدار الإصدار الخاص بي للمنتج:

 public class ProductBuilder { private Product _product; public ProductBuilder(string name) { _product = new Product(name); } public ProductBuilder Containing(Ingredient ingredient) { _product.AddIngredient(ingredient); return this; } public Product Please() { return _product; } } 

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

سيتيح لك مصمم المنتجات إنشاء تصميمات مثل:

 var pepperoni = new ProductBuilder("Pepperoni") .Containing(Ingredients.Dough) .Containing(Ingredients.Pepperoni) .Please(); 

تساعدك البنيات على إنشاء كائنات تحتاج إلى التخصيص. النظر في إنشاء منشئ حتى لو كان التكوين يتكون من سطر واحد.

ObjectMother الكائن


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

النمط بسيط مثل 5 كوبيل: نقوم بإنشاء فئة ثابتة ونجمع كل البناة فيها.

 public static class Create { public static ProductBuilder Product(string name) => new ProductBuilder(name); } 

الآن يمكنك كتابة مثل هذا:

 var pepperoni = Create.Product("Pepperoni") .Containing(Ingredients.Dough) .Containing(Ingredients.Pepperoni) .Please(); 

تم اختراع ObjectMother لإنشاء الكائنات. بالإضافة إلى ذلك ، فإنه يساعد على إدخال مطورين جدد في المجال ، مثل عند كتابة الكلمة Create IDE نفسها ، ستخبرك بما يمكنك إنشاؤه في هذا المجال.

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

إزالة المنتج


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

 public static class Pizza { public static Product Pepperoni => Create.Product("Pepperoni") .Containing(Ingredients.Dough) .Containing(Ingredients.Pepperoni) .Please(); public static Product Margarita => Create.Product("Margarita") .Containing(Ingredients.Dough) .Containing(Ingredients.Mozzarella) .Please(); } 

هنا اتصلت الحاوية لا Products ، ولكن Pizza . هذا الاسم يساعد على قراءة الاختبار. على سبيل المثال ، يساعد على إزالة أسئلة مثل "هل بيبيروني بيتزا أم نقانق؟".
حاول استخدام كائنات مجال حقيقية ، وليس بدائل مثل Product1.

باني الطلب (مثال من الخلف)


نحن الآن نطبق الأنماط الموصوفة لإنشاء أداة إنشاء أوامر ، لكن الآن لننتقل من أداة البناء ، ولكن من الأشياء التي نود تلقيها. هكذا أرغب في إنشاء طلب:

 var order = Create.Order .Dated(DateTime.Now) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please(); 

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

 public class OrderBuilder { private DateTime _date; private readonly List<OrderLine> _lines = new List<OrderLine>(); public OrderBuilder Dated(DateTime date) { _date = date; return this; } public OrderBuilder With(OrderLine orderLine) { _lines.Add(orderLine); return this; } public Order Please() { var order = new Order(_date); foreach (var line in _lines) { order.AddLine(line); } return order; } } 

لكن بالنسبة لـ OrderLine الموقف أكثر إثارة للاهتمام: أولاً ، لا تُسمى هنا طريقة رجاءً ، وثانياً ، لا يتم توفير الوصول إلى المنشئ من خلال Create ثابت وليس منشئ المنشئ نفسه. سنحل المشكلة الأولى باستخدام implicit operator المنشئ الخاص بنا كما يلي:

 public class OrderLineBuilder { private Product _product; private decimal _count; private decimal _price; public OrderLineBuilder Of(decimal count, Product product) { _product = product; _count = count; return this; } public OrderLineBuilder For(decimal price) { _price = price; return this; } public static implicit operator OrderLine(OrderLineBuilder b) { return new OrderLine(b._product, b._count, b._price); } } 

الطريقة الثانية ستساعدنا على فهم طريقة الامتداد لفئة Product :

 public static class ProductExtensions { public static OrderLineBuilder CountOf(this Product product, decimal count) { return Create.OrderLine.Of(count, product) } } 

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

 [Fact] public void AcceptOrder_Successful() { var order = Create.Order .Dated(DateTime.Now) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please(); var orderRepositoryMock = new Mock<IOrderRepository>(); var ingredientsRepositoryMock = new Mock<IIngredientRepository>(); var service = new PizzeriaService(orderRepositoryMock.Object, ingredientsRepositoryMock.Object); service.AcceptOrder(order); orderRepositoryMock.Verify(r => r.Add(order), Times.Once); ingredientsRepositoryMock.Verify(r => r.ReserveIngredients(order), Times.Once); } 

هنا اتخذنا النهج الذي نسميه "الجنية الجنية". هذا هو عندما تكتب شفرة الخمول لأول مرة كما تود رؤيتها ، ثم تحاول التفاف ما كتبته في DSL. هذا مفيد جدًا للتصرف - في بعض الأحيان لا يمكنك أن تتخيل ما يمكن لـ C # فعله.
تخيل أن جنية سحرية قد وصلت وسمحت لك بكتابة الشفرة كما تريد ، ثم حاول التفاف كل شيء مكتوب في DSL.

إنشاء خدمة (نمط قابل للاختبار)


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

لذلك ، أريد التأكد من عدم وجود موك واحد في طريقة الاختبار الخاصة بي. للقيام بذلك ، سوف أقوم بتكوين غلاف لـ PizzeriaService حيث أقوم بتغليف كل المنطق الذي يتحقق من مكالمات الطريقة:

 public class PizzeriaServiceTestable : PizzeriaService { private readonly Mock<IOrderRepository> _orderRepositoryMock; private readonly Mock<IIngredientRepository> _ingredientRepositoryMock; public PizzeriaServiceTestable(Mock<IOrderRepository> orderRepositoryMock, Mock<IIngredientRepository> ingredientRepositoryMock) : base(orderRepositoryMock.Object, ingredientRepositoryMock.Object) { _orderRepositoryMock = orderRepositoryMock; _ingredientRepositoryMock = ingredientRepositoryMock; } public void VerifyAddWasCalledWith(Order order) { _orderRepositoryMock.Verify(r => r.Add(order), Times.Once); } public void VerifyReserveIngredientsWasCalledWith(Order order) { _ingredientRepositoryMock.Verify(r => r.ReserveIngredients(order), Times.Once); } } 

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

 public class PizzeriaServiceBuilder { public PizzeriaServiceTestable Please() { var orderRepositoryMock = new Mock<IOrderRepository>(); var ingredientsRepositoryMock = new Mock<IIngredientRepository>(); return new PizzeriaServiceTestable(orderRepositoryMock, ingredientsRepositoryMock); } } 

في الوقت الحالي ، تبدو طريقة الاختبار لدينا كما يلي:

 [Fact] public void AcceptOrder_Successful() { var order = Create.Order .Dated(DateTime.Now) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please(); var service = Create.PizzeriaService.Please(); service.AcceptOrder(order); service.VerifyAddWasCalledWith(order); service.VerifyReserveIngredientsWasCalledWith(order); } 

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

استنساخ (التمديد الحرفي)


لا يرتبط نمط الامتداد الحرفي ارتباطًا مباشرًا باستنساخه ، لكنه سيساعدنا في ذلك. مشكلتنا في الوقت الحالي هي أننا نستخدم DateTime.Now التاريخ كتاريخ الطلب. إذا بدأ فجأة من تاريخ ما ، فإن منطق قبول الطلب يتغير ، ثم في منطق أعمالنا ، سيتعين علينا على الأقل لبعض الوقت دعم if (order.Date > edgeDate) لقبول الطلب ، مع فصلهما عن طريق التحقق مثل if (order.Date > edgeDate) . في هذه الحالة ، يتمتع اختبارنا بفرصة السقوط عندما يمر تاريخ النظام عبر الحدود. نعم ، سنصلح هذا الأمر بسرعة ، ونجعل اختبارين من أحدهما: أحدهما سوف يتحقق من المنطق قبل تاريخ الحدود ، والآخر بعده. ومع ذلك ، من الأفضل تجنب مثل هذه المواقف وجعل جميع بيانات الإدخال ثابتة على الفور.

"وأين DSL؟" - أنت تسأل. الحقيقة هي أنه من المناسب إدخال التواريخ في الاختبارات من خلال طرق الإرشاد ، على سبيل المثال 3.May(2019) . سيكون هذا الشكل من التسجيل مفهومًا ليس فقط للمطورين ، ولكن أيضًا للشركات. للقيام بذلك ، فقط قم بإنشاء فئة ثابتة

 public static class DateConstructionExtensions { public static DateTime May(this int day, int year) => new DateTime(year, 5, day); } 

بطبيعة الحال ، ليست التواريخ هي الأشياء الوحيدة التي تستخدم هذا النمط من أجل. على سبيل المثال ، إذا أدخلنا كمية المكونات في تركيبة المنتجات ، فيمكننا كتابة شيء مثل 42.Grams("flour") .
يتم إنشاء الكائنات والتواريخ الكمية بسهولة من خلال طرق التمديد المألوفة.

تركيز


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

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

 [Fact] public void WhenAcceptOrder_AddIsCalled() { var order = Create.Order .Dated(3.May(2019)) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please(); var service = Create.PizzeriaService.Please(); service.AcceptOrder(order); service.VerifyAddWasCalledWith(order); } [Fact] public void WhenAcceptOrder_ReserveIngredientsIsCalled() { var order = Create.Order .Dated(3.May(2019)) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please(); var service = Create.PizzeriaService.Please(); service.AcceptOrder(order); service.VerifyReserveIngredientsWasCalledWith(order); } 

تبين أن كلا الاختبارين قصير وواضح وقابل للتكرار ومركّز.

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

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

عينة شفرة المصدر والاختبارات المتاحة هنا .

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


All Articles