خفض مستوى الاتصال باستخدام DI لتحسين اختبار الرمز ، مثال على التنفيذ

في بداية المقال ، أود أن أشير على الفور إلى أنني لا أتظاهر بأنني جديد ، لكنني أريد فقط مشاركة / استدعاء هذه الفرصة مثل IoC DI.


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


ما نتحدث عنه


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


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


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


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


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


الفكرة العامة لكيفية حل هذا في مشاريع روبي


الفكرة الأساسية هي أنه بالنسبة للأشياء التي لها تبعيات ، يجب أن نكون قادرين على إدارتها.


فكر في مثال:


class UserService def initialize() @notification_service = NotificationService.new end def block_user(user) user.block! @notification_service.send(user, 'you have been blocked') end end 

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


 class UserService def initialize(notification_service = NotificationService.new) @notification_service = notification_service end def block_user(user) user.block! @notification_service.send(user, 'you have been blocked') end end 

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


 RSpec.describe UserService, type: :service do let (:notification_service) { instance_double(NotificationService) } let (:service) { UserService.new(notification_service) } describe ".block_user" do let (:user) { instance_double(User) } it "should block user and send notification" do expect(user).to receive :block! expect(notification_service).to receive(:send).with(user, "you have been blocked") service.block_user(user) end end end 

دراسة حالة للسكك الحديدية


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


في هذا الصدد ، حدث لي أن أكتب وحدة صغيرة تقوم بأتمتة إدارة التبعية.


 module Services module Injector def self.included(base) # TODO: check base, should be controller or service base.extend ClassMethods end module ClassMethods def inject_service(name) service = Services::Helpers::Service.new(name) attr_writer service.to_s define_method service.to_s do instance_variable_get("@#{service.to_s}").tap { |s| return s if s } instance_variable_set("@#{service.to_s}", service.instance) end end end end module Helpers class Service def initialize(name) raise ArgumentError, 'name of service should be a Symbol' unless name.is_a? Symbol @name = name.to_s.downcase @class = "#{@name.camelcase}Service".constantize unless @class.respond_to? :instance raise ArgumentError, "#{@name.to_s} should be singleton (respond to instance method)" end end def to_s "#{@name}_service" end def instance if Rails.env.test? if defined? RSpec::Mocks::ExampleMethods extend RSpec::Mocks::ExampleMethods instance_double @class else nil end else @class.instance end end end end end 

هناك تحذير واحد ، يجب أن تكون الخدمة Singleton ، أي لديك طريقة مثيل. أسهل طريقة للقيام بذلك هي عن طريق الكتابة include Singleton في فئة الخدمة.


الآن في إضافة ApplicationController


 require 'services' class ApplicationController < ActionController::Base include Services::Injector end 

والآن في وحدات التحكم يمكننا القيام بذلك


 class WelcomeController < ApplicationController inject_service :welcome def index render plain: welcome_service.text end end 

في مواصفات وحدة التحكم هذه ، نحصل تلقائيًا على example_double (خدمة ترحيب مرحبًا) كمُعال.


 RSpec.describe WelcomeController, type: :controller do describe "index" do it "should render text from test service" do allow(controller.welcome_service).to receive(:text).and_return "OK" get :index expect(response).to have_attributes body: "OK" expect(response).to have_http_status 200 end end end 

ما يمكن تحسينه


تخيل ، على سبيل المثال ، أنه في نظامنا هناك العديد من الخيارات لكيفية إرسال الإشعارات ، على سبيل المثال ، في الليل سيكون مزودًا وآخر خلال النهار. في الوقت نفسه ، لدى مزودي الخدمة بروتوكولات إرسال مختلفة تمامًا.


بشكل عام ، تظل واجهة NotificationService كما هي ، ولكن هناك تطبيقان محددان.


 class NightlyNotificationService < NotificationService end class DailyNotificationService < NotificationService end 

الآن يمكننا كتابة فصل دراسي يقوم بإجراء رسم خرائط مشروط للخدمات


 class NotificationServiceMapper include Singleton def take now = Time.now ((now.hour >= 00) and (now.hour <= 8)) ? NightlyNotificationService : DailyNotificationService end end 

الآن عندما نأخذ مثيل الخدمة في Services :: Helpers :: Service.instance نحتاج إلى التحقق مما إذا كان هناك كائن * Mapper ، وإذا كان الأمر كذلك ، فعليك أخذ الفصل الثابت من خلال أخذ.

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


All Articles