Linters ดำเนินการโดย Ruby Libraries
เผยแพร่แล้ว: 2022-03-11เมื่อคุณได้ยินคำว่า " linter " หรือ " lint " คุณคงมีความคาดหวังอยู่แล้วว่าเครื่องมือดังกล่าวทำงานอย่างไรหรือควรทำอย่างไร
คุณอาจกำลังนึกถึง Rubocop ซึ่งผู้พัฒนา Toptal คนใดคนหนึ่งดูแล หรือของ JSLint, ESLint หรือสิ่งที่ไม่ค่อยเป็นที่รู้จักหรือไม่ค่อยเป็นที่นิยม
บทความนี้จะแนะนำคุณเกี่ยวกับ Linters ประเภทต่างๆ พวกเขาไม่ตรวจสอบไวยากรณ์ของโค้ดและไม่ตรวจสอบ Abstract-Syntax-Tree แต่จะตรวจสอบโค้ด พวกเขาตรวจสอบว่าการใช้งานเป็นไปตามอินเทอร์เฟซบางอย่างหรือไม่ ไม่ใช่แค่คำศัพท์ (ในแง่ของการพิมพ์แบบเป็ดและอินเทอร์เฟซแบบคลาสสิก) แต่บางครั้งก็มีความหมายด้วย
เพื่อทำความคุ้นเคยกับพวกเขา มาวิเคราะห์ตัวอย่างที่ใช้งานได้จริง หากคุณไม่ใช่มืออาชีพ Rails ตัวยง คุณอาจต้องการอ่านสิ่งนี้ก่อน
มาเริ่มกันที่ Lint พื้นฐานกัน
ActiveModel::Lint::Tests
พฤติกรรมของผ้าสำลีนี้อธิบายโดยละเอียดในเอกสารทางการของ Rails:
“คุณสามารถทดสอบว่าวัตถุนั้นสอดคล้องกับ Active Model API หรือไม่ โดยรวม 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 endActiveModel::Serializer::Lint::Tests
เครื่องซีเรียลไลเซอร์รุ่นที่ใช้งานไม่ใช่เรื่องใหม่ แต่เราสามารถเรียนรู้จากพวกเขาต่อไปได้ คุณรวม ActiveModel::Serializer::Lint::Tests เพื่อตรวจสอบว่าวัตถุสอดคล้องกับ Active Model Serializers API หรือไม่ หากไม่เป็นเช่นนั้น การทดสอบจะระบุว่าส่วนใดขาดหายไป
อย่างไรก็ตาม ในเอกสาร คุณจะพบคำเตือนที่สำคัญว่าไม่ตรวจสอบความหมาย:
“การทดสอบเหล่านี้ไม่ได้พยายามกำหนดความถูกต้องตามความหมายของค่าที่ส่งคืน ตัวอย่างเช่น คุณสามารถใช้ serializable_hash เพื่อส่งคืน {} เสมอ และการทดสอบจะผ่านไป มันขึ้นอยู่กับคุณที่จะทำให้แน่ใจว่าค่านิยมมีความหมายเชิงความหมาย”
กล่าวคือ เรากำลังตรวจสอบรูปร่างของอินเทอร์เฟซเท่านั้น ตอนนี้เรามาดูกันว่ามันใช้งานอย่างไร
การดำเนินการ
สิ่งนี้คล้ายกันมากกับสิ่งที่เราเห็นเมื่อสักครู่นี้ด้วยการใช้งาน ActiveModel::Lint::Tests แต่เข้มงวดกว่าเล็กน้อยในบางกรณีเนื่องจากจะตรวจสอบ arity หรือคลาสของค่าที่ส่งคืน:
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 เป็นสัตว์ร้ายที่ต่างไปจากเดิมอย่างสิ้นเชิง มิดเดิลแวร์ของแร็คเป็นแร็คที่คุณสามารถห่อหุ้มแอปพลิเคชันของคุณได้ มิดเดิลแวร์จะทำหน้าที่เป็นตัวคั่นกลางในกรณีนี้ linter จะตรวจสอบว่าคำขอและการตอบสนองถูกสร้างขึ้นตามข้อกำหนดของ 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
ในตัวอย่างนี้ เราจะเห็นว่า 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 - ที่เก็บ Lint
Rails Event Store เป็นไลบรารีสำหรับเผยแพร่ ใช้งาน จัดเก็บ และเรียกข้อมูลกิจกรรม มีจุดมุ่งหมายเพื่อช่วยคุณในการใช้สถาปัตยกรรมที่ขับเคลื่อนด้วยเหตุการณ์สำหรับแอปพลิเคชัน Rails ของคุณ เป็นไลบรารีแบบโมดูลาร์ที่สร้างขึ้นด้วยส่วนประกอบขนาดเล็ก เช่น ที่เก็บข้อมูล ผู้ทำแผนที่ ผู้มอบหมายงาน ตัวจัดกำหนดการ การสมัครรับข้อมูล และเครื่องซีเรียลไลเซอร์ แต่ละองค์ประกอบสามารถมีการใช้งานแทนกันได้
ตัวอย่างเช่น ที่เก็บเริ่มต้นใช้ 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ที่โค้ด Ruby เกือบ 1,400 บรรทัด ฉันเชื่อว่ามันเป็น linter ที่ใหญ่ที่สุดที่เขียนด้วย Ruby แต่ถ้ารู้ว่าใหญ่กว่านี้ บอกได้นะคะ ส่วนที่น่าสนใจคือมันเกี่ยวกับความหมาย 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: ทำให้ Ruby Object รู้สึกเหมือน ActiveRecord
- RailsCast เกี่ยวกับ Active Model
- การใช้ผ้าสำลีแบบเต็มแร็ค
- การใช้แร็คผ้าสำลีใน Puma
- เรียนรู้เพิ่มเติมเกี่ยวกับ Rails Event Store
