وقتی کلمه “لنگر” یا “پرزاحتمالاً از قبل انتظارات خاصی در مورد نحوه عملکرد چنین ابزاری یا کاری که باید انجام دهد دارید.
ممکن است به این فکر کنید روبوکاپ، که یکی از توسعه دهندگان Toptal حفظ می کند، یا از JSLint، ESLint، یا چیزی کمتر شناخته شده یا کمتر محبوب است.
این مقاله شما را با انواع مختلفی از لینترها آشنا می کند. آنها نه نحو کد را بررسی می کنند و نه Abstract-Syntax-Tree را تأیید می کنند، اما کد را تأیید می کنند. آنها بررسی می کنند که آیا یک پیاده سازی به یک رابط خاص پایبند است، نه تنها از نظر لغوی (از نظر تایپ اردک و رابط های کلاسیک) بلکه گاهی اوقات نیز از نظر معنایی.
برای آشنایی با آنها، اجازه دهید چند مثال کاربردی را تحلیل کنیم. اگر حرفه ای مشتاق Rails نیستید، ممکن است بخواهید ابتدا این را بخوانید.
بیایید با یک لینت اولیه شروع کنیم.
ActiveModel::Lint::تست ها
رفتار این لینت به تفصیل در توضیح داده شده است اسناد رسمی ریل:
“شما می توانید تست کنید که آیا یک شی با API Active Model مطابقت دارد یا خیر ActiveModel::Lint::Tests
در شما TestCase
. شامل تستهایی میشود که به شما میگوید آیا شیء شما کاملاً سازگار است یا اگر نه، کدام جنبههای API پیادهسازی نشدهاند. توجه داشته باشید که برای پیاده سازی همه APIها برای کار با 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
end
ActiveModel::Serializer::Lint::تست ها
سریالسازهای مدل اکتیو جدید نیستند، اما میتوانیم از آنها یاد بگیریم. شما شامل می شوید 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 است که می توانید برنامه خود را در آن بپیچید. لینتر بررسی می کند که آیا درخواست ها و پاسخ ها مطابق با مشخصات Rack ساخته شده اند یا خیر. اگر از یک سرور 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"]] }
اول در یک 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 = "
@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 – Repository Lint
فروشگاه رویداد ریل کتابخانه ای برای انتشار، مصرف، ذخیره و بازیابی رویدادها است. هدف آن کمک به شما در اجرای معماری رویداد محور برای برنامه Rails شماست. این یک کتابخانه مدولار است که با اجزای کوچکی مانند مخزن، نقشهبردار، توزیعکننده، زمانبندی، اشتراکها و سریالساز ساخته شده است. هر جزء می تواند یک پیاده سازی قابل تعویض داشته باشد.
برای مثال، مخزن پیشفرض از ActiveRecord استفاده میکند و طرحبندی جدول خاصی را برای ذخیره رویدادها در نظر میگیرد. با این حال، پیاده سازی شما می تواند از رام یا کار استفاده کند در حافظه بدون ذخیره رویدادها، که برای آزمایش مفید است.
اما چگونه میتوانید بفهمید که آیا مؤلفهای که پیادهسازی کردهاید به گونهای رفتار میکند که کتابخانه انتظار دارد؟ با استفاده از لینتر ارائه شده، البته. و بسیار زیاد است. حدود 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٪ در مورد معناشناسی است.
این رابط کاربری را نیز به شدت آزمایش میکند، اما میتوانم بگویم که با توجه به دامنه این مقاله، یک فکر بعدی است.
پیاده سازی
لینتر مخزن با استفاده از نمونه های مشترک RSpec عملکرد:
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
# ...
استفاده
این لینتر، مانند سایرین، از شما انتظار دارد که برخی از روش ها را ارائه دهید، مهمتر از همه repository
، که پیاده سازی را برای تأیید برمی گرداند. نمونه های آزمایشی با استفاده از RSpec داخلی گنجانده شده اند include_examples
روش:
RSpec.describe EventRepository do
include_examples :event_repository
let(:repository) { EventRepository.new(serializer: YAML) }
end
بسته بندی
همانطور که می بینید، “لنگر” معنای کمی گسترده تر از آنچه ما معمولاً در ذهن داریم دارد. هر زمان که کتابخانهای را پیادهسازی میکنید که انتظار برخی از همکاران قابل تعویض را دارد، من شما را تشویق میکنم که یک لینتر تهیه کنید.
حتی اگر تنها کلاسی که در ابتدا چنین آزمون هایی را گذرانده است، کلاسی باشد که توسط کتابخانه شما ارائه می شود، این نشانه آن است که شما به عنوان یک مهندس نرم افزار توسعه پذیری را جدی می گیرید. همچنین شما را به چالش می کشد تا در مورد رابط هر جزء در کد خود فکر کنید، نه تصادفی بلکه آگاهانه.