لينترز نفذتها مكتبات روبي
نشرت: 2022-03-11عندما تسمع كلمة " linter " أو " lint " ، فمن المحتمل أن يكون لديك بالفعل توقعات معينة حول كيفية عمل هذه الأداة أو ما يجب أن تفعله.
قد تفكر في Rubocop ، الذي يحتفظ به أحد مطوري Toptal ، أو JSLint أو ESLint أو شيء أقل شهرة أو أقل شهرة.
ستقدم لك هذه المقالة نوعًا مختلفًا من الوبر. إنهم لا يتحققون من بناء جملة الكود ولا يتحققون من شجرة Abstract-Syntax-Tree ، لكنهم يتحققون من الكود. إنهم يتحققون مما إذا كان التطبيق يلتزم بواجهة معينة ، ليس فقط بشكل معجمي (من حيث كتابة البط والواجهات الكلاسيكية) ولكن أحيانًا أيضًا من الناحية الدلالية.
للتعرف عليها ، دعنا نحلل بعض الأمثلة العملية. إذا لم تكن محترفًا في ريلز ، فقد ترغب في قراءة هذا أولاً.
لنبدأ مع Lint الأساسي.
ActiveModel :: Lint :: Tests
تم شرح سلوك هذا Lint بالتفصيل في وثائق ريلز الرسمية:
"يمكنك اختبار ما إذا كان كائن ما متوافقًا مع Active Model API عن طريق تضمين ActiveModel::Lint::Tests في TestCase الخاص بك. سيتضمن اختبارات تخبرك ما إذا كان الكائن الخاص بك متوافقًا تمامًا أو ، إذا لم يكن كذلك ، ما هي جوانب واجهة برمجة التطبيقات التي لم يتم تنفيذها. لاحظ أن الكائن ليس مطلوبًا لتنفيذ كافة واجهات برمجة التطبيقات للعمل مع Action Pack. تهدف هذه الوحدة فقط إلى تقديم إرشادات في حالة رغبتك في إخراج جميع الميزات من العلبة ".
لذلك ، إذا كنت تقوم بتطبيق فصل وترغب في استخدامه مع وظائف Rails الحالية مثل redirect_to, form_for ، فأنت بحاجة إلى تنفيذ طريقتين. لا تقتصر هذه الوظيفة على كائنات ActiveRecord . يمكن أن يعمل مع الأشياء الخاصة بك أيضًا ، لكنهم بحاجة إلى تعلم الدجل بشكل صحيح.
تطبيق
التنفيذ بسيط نسبيًا. إنها وحدة تم إنشاؤها ليتم تضمينها في حالات الاختبار. سيتم تنفيذ الطرق التي تبدأ بـ test_ بواسطة إطار العمل الخاص بك. من المتوقع أن يتم إعداد متغير مثيل @model بواسطة المستخدم قبل الاختبار:
module ActiveModel module Lint module Tests def test_to_key assert_respond_to model, :to_key def model.persisted?() false end assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false" end def test_to_param assert_respond_to model, :to_param def model.to_key() [1] end def model.persisted?() false end assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false" end ... private def model assert_respond_to @model, :to_model @model.to_model endإستعمال
class Person def persisted? false end def to_key nil end def to_param nil end # ... end # test/models/person_test.rb require "test_helper" class PersonTest < ActiveSupport::TestCase include ActiveModel::Lint::Tests setup do @model = Person.new end endActiveModel :: Serializer :: Lint :: Tests
المسلسلات النموذجية النشطة ليست جديدة ، ولكن يمكننا الاستمرار في التعلم منها. تقوم بتضمين ActiveModel::Serializer::Lint::Tests للتحقق مما إذا كان الكائن متوافقًا مع Active Model Serializers API. إذا لم يكن الأمر كذلك ، فستشير الاختبارات إلى الأجزاء المفقودة.
ومع ذلك ، ستجد في المستندات تحذيرًا مهمًا بأنه لا يتحقق من الدلالات:
"لا تحاول هذه الاختبارات تحديد الدقة الدلالية للقيم التي تم إرجاعها. على سبيل المثال ، يمكنك تنفيذ serializable_hash لإرجاع {} دائمًا ، وستنجح الاختبارات. الأمر متروك لك للتأكد من أن القيم ذات مغزى ".
بمعنى آخر ، نحن نتحقق فقط من شكل الواجهة. الآن دعونا نرى كيف يتم تنفيذه.
تطبيق
هذا مشابه جدًا لما رأيناه منذ لحظة مع تنفيذ ActiveModel::Lint::Tests ، ولكنه أكثر صرامة في بعض الحالات لأنه يتحقق من أصناف القيم المرتجعة أو فئاتها:
module ActiveModel class Serializer module Lint module Tests # Passes if the object responds to <tt>read_attribute_for_serialization</tt> # and if it requires one argument (the attribute to be read). # Fails otherwise. # # <tt>read_attribute_for_serialization</tt> gets the attribute value for serialization # Typically, it is implemented by including ActiveModel::Serialization. def test_read_attribute_for_serialization assert_respond_to resource, :read_attribute_for_serialization, 'The resource should respond to read_attribute_for_serialization' actual_arity = resource.method(:read_attribute_for_serialization).arity # using absolute value since arity is: # 1 for def read_attribute_for_serialization(name); end # -1 for alias :read_attribute_for_serialization :send assert_equal 1, actual_arity.abs, "expected #{actual_arity.inspect}.abs to be 1 or -1" end # Passes if the object's class responds to <tt>model_name</tt> and if it # is in an instance of +ActiveModel::Name+. # Fails otherwise. # # <tt>model_name</tt> returns an ActiveModel::Name instance. # It is used by the serializer to identify the object's type. # It is not required unless caching is enabled. def test_model_name resource_class = resource.class assert_respond_to resource_class, :model_name assert_instance_of resource_class.model_name, ActiveModel::Name end ...إستعمال
فيما يلي مثال على كيفية استخدام ActiveModelSerializers في النسالة من خلال تضمينها في حالة الاختبار الخاصة بها:
module ActiveModelSerializers class ModelTest < ActiveSupport::TestCase include ActiveModel::Serializer::Lint::Tests setup do @resource = ActiveModelSerializers::Model.new end def test_initialization_with_string_keys klass = Class.new(ActiveModelSerializers::Model) do attributes :key end value = 'value' model_instance = klass.new('key' => value) assert_equal model_instance.read_attribute_for_serialization(:key), value endالرف :: لينت
الأمثلة السابقة لم تهتم بدلالات الألفاظ .
ومع ذلك ، فإن Rack::Lint هو وحش مختلف تمامًا. يمكنك تغليف تطبيقك ببرنامج Rack الوسيط. تلعب البرامج الوسيطة دور linter في هذه الحالة. سيتحقق linter مما إذا كانت الطلبات والاستجابات يتم إنشاؤها وفقًا لمواصفات الرف. يكون هذا مفيدًا إذا كنت تقوم بتنفيذ خادم Rack (على سبيل المثال ، Puma) يخدم تطبيق Rack وتريد التأكد من اتباعك لمواصفات Rack.
بدلاً من ذلك ، يتم استخدامه عند تنفيذ تطبيق مكشوف للغاية وتريد التأكد من أنك لا ترتكب أخطاء بسيطة تتعلق ببروتوكول HTTP.
تطبيق
module Rack class Lint def initialize(app) @app = app @content_length = nil end def call(env = nil) dup._call(env) end def _call(env) raise LintError, "No env given" unless env check_env env env[RACK_INPUT] = InputWrapper.new(env[RACK_INPUT]) env[RACK_ERRORS] = ErrorWrapper.new(env[RACK_ERRORS]) ary = @app.call(env) raise LintError, "response is not an Array, but #{ary.class}" unless ary.kind_of? Array raise LintError, "response array has #{ary.size} elements instead of 3" unless ary.size == 3 status, headers, @body = ary check_status status check_headers headers hijack_proc = check_hijack_response headers, env if hijack_proc && headers.is_a?(Hash) headers[RACK_HIJACK] = hijack_proc end check_content_type status, headers check_content_length status, headers @head_request = env[REQUEST_METHOD] == HEAD [status, headers, self] end ## === The Content-Type def check_content_type(status, headers) headers.each { |key, value| ## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx, 204 or 304. if key.downcase == "content-type" if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i raise LintError, "Content-Type header found in #{status} response, not allowed" end return end } end ## === The Content-Length def check_content_length(status, headers) headers.each { |key, value| if key.downcase == 'content-length' ## There must not be a <tt>Content-Length</tt> header when the +Status+ is 1xx, 204 or 304. if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i raise LintError, "Content-Length header found in #{status} response, not allowed" end @content_length = value end } end ...الاستخدام في التطبيق الخاص بك
لنفترض أننا أنشأنا نقطة نهاية بسيطة للغاية. في بعض الأحيان يجب أن يرد بعبارة "لا يوجد محتوى" ، لكننا ارتكبنا خطأً متعمدًا وسنرسل بعض المحتوى في 50٪ من الحالات:

# foo.rb # run with rackup foo.rb Foo = Rack::Builder.new do use Rack::Lint use Rack::ContentLength app = proc do |env| if rand > 0.5 no_content = Rack::Utils::HTTP_STATUS_CODES.invert['No Content'] [no_content, { 'Content-Type' => 'text/plain' }, ['bummer no content with content']] else ok = Rack::Utils::HTTP_STATUS_CODES.invert['OK'] [ok, { 'Content-Type' => 'text/plain' }, ['good']] end end run app end.to_app في مثل هذه الحالات ، سيقوم Rack::Lint باعتراض الاستجابة والتحقق منها وإثارة استثناء:
Rack::Lint::LintError: Content-Type header found in 204 response, not allowed /Users/dev/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/lint.rb:21:in `assert' /Users/dev/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/lint.rb:710:in `block in check_content_type'الاستخدام في بوما
في هذا المثال ، نرى كيف يلف Puma تطبيقًا بسيطًا للغاية lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] } lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] } أولاً في ServerLint (الذي يرث من Rack::Lint ) ثم في ErrorChecker .
يثير النسالة استثناءات في حالة عدم اتباع المواصفات. يلتقط المدقق الاستثناءات ويعيد رمز الخطأ 500. يتحقق رمز الاختبار من عدم حدوث الاستثناء:
class TestRackServer < Minitest::Test class ErrorChecker def initialize(app) @app = app @exception = nil end attr_reader :exception, :env def call(env) begin @app.call(env) rescue Exception => e @exception = e [ 500, {}, ["Error detected"] ] end end end class ServerLint < Rack::Lint def call(env) check_env env @app.call(env) end end def setup @simple = lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] } @server = Puma::Server.new @simple port = (@server.add_tcp_listener "127.0.0.1", 0).addr[1] @tcp = "http://127.0.0.1:#{port}" @stopped = false end def test_lint @checker = ErrorChecker.new ServerLint.new(@simple) @server.app = @checker @server.run hit(["#{@tcp}/test"]) stop refute @checker.exception, "Checker raised exception" endهذه هي الطريقة التي يتم بها التحقق من Puma لتكون متوافقة مع Rack.
RailsEventStore - مستودع لينت
Rails Event Store هي مكتبة لنشر الأحداث واستهلاكها وتخزينها واسترجاعها. تهدف إلى مساعدتك في تنفيذ الهندسة المعمارية التي تعتمد على الأحداث لتطبيق ريلز الخاص بك. إنها مكتبة معيارية مبنية بمكونات صغيرة مثل المستودع ، ومخطط الخرائط ، والمرسل ، والمجدول ، والاشتراكات ، والمسلسل. يمكن أن يكون لكل مكون تنفيذ قابل للتبديل.
على سبيل المثال ، يستخدم المستودع الافتراضي ActiveRecord ويفترض تخطيط جدول معين لتخزين الأحداث. ومع ذلك ، يمكن للتنفيذ الخاص بك استخدام ROM أو العمل في الذاكرة دون تخزين الأحداث ، وهو أمر مفيد للاختبار.
ولكن كيف يمكنك معرفة ما إذا كان المكون الذي نفذته يتصرف بطريقة تتوقعها المكتبة؟ باستخدام لينتر المقدم بالطبع. وهي هائلة. ويغطي حوالي 80 حالة. بعضها بسيط نسبيًا:
specify 'adds an initial event to a new stream' do repository.append_to_stream([event = SRecord.new], stream, version_none) expect(read_events_forward(repository).first).to eq(event) expect(read_events_forward(repository, stream).first).to eq(event) expect(read_events_forward(repository, stream_other)).to be_empty endوبعضها أكثر تعقيدًا بعض الشيء ويتعلق بمسارات غير سعيدة:
it 'does not allow linking same event twice in a stream' do repository.append_to_stream( [SRecord.new(event_id: "a1b49edb")], stream, version_none ).link_to_stream(["a1b49edb"], stream_flow, version_none) expect do repository.link_to_stream(["a1b49edb"], stream_flow, version_0) end.to raise_error(EventDuplicatedInStream) endفي ما يقرب من 1400 سطر من كود روبي ، أعتقد أنه أكبر لينتر مكتوب بلغة روبي. ولكن إذا كنت على دراية بأحد أكبر ، فأخبرني بذلك. الجزء المثير للاهتمام هو أنه يتعلق 100٪ بالدلالات.
إنه يختبر الواجهة أيضًا بشكل كبير ، لكنني أود أن أقول إن هذه فكرة متأخرة بالنظر إلى نطاق هذه المقالة.
تطبيق
يتم تنفيذ مستودع linter باستخدام وظيفة RSpec Shared Examples:
module RubyEventStore ::RSpec.shared_examples :event_repository do let(:helper) { EventRepositoryHelper.new } let(:specification) { Specification.new(SpecificationReader.new(repository, Mappers::NullMapper.new)) } let(:global_stream) { Stream.new(GLOBAL_STREAM) } let(:stream) { Stream.new(SecureRandom.uuid) } let(:stream_flow) { Stream.new('flow') } # ... it 'just created is empty' do expect(read_events_forward(repository)).to be_empty end specify 'append_to_stream returns self' do repository .append_to_stream([event = SRecord.new], stream, version_none) .append_to_stream([event = SRecord.new], stream, version_0) end # ...إستعمال
هذا linter ، مثل الآخرين ، يتوقع منك توفير بعض الطرق ، وأهمها repository ، الذي يعيد التنفيذ ليتم التحقق منه. يتم تضمين أمثلة الاختبار باستخدام طريقة RSpec include_examples المضمنة:
RSpec.describe EventRepository do include_examples :event_repository let(:repository) { EventRepository.new(serializer: YAML) } endتغليف
كما ترى ، " linter" لها معنى أوسع قليلاً مما يدور في أذهاننا عادةً. في أي وقت تقوم فيه بتنفيذ مكتبة تتوقع بعض المتعاونين القابلين للتبادل ، أشجعك على التفكير في توفير linter.
حتى لو كان الفصل الوحيد الذي يجتاز مثل هذه الاختبارات في البداية هو فصل توفره مكتبتك أيضًا ، فهذه علامة على أنك كمهندس برمجيات تأخذ قابلية التوسع على محمل الجد. سوف يتحداك أيضًا أن تفكر في واجهة كل مكون في الكود الخاص بك ، ليس عن طريق الصدفة ولكن بوعي.
موارد
- ActiveModel: اجعل أي كائن روبي يشعر وكأنه تسجيل نشط
- RailsCast حول النموذج النشط
- تنفيذ كامل للحامل الوبر
- استخدام Rack Lint في Puma
- تعرف على المزيد حول Rails Event Store
