Ruby Kitaplıkları Tarafından Uygulanan Linterler

Yayınlanan: 2022-03-11

Linter ” veya “ lint ” kelimesini duyduğunuzda, muhtemelen böyle bir aracın nasıl çalıştığı veya ne yapması gerektiği konusunda belirli beklentileriniz vardır.

Bir Toptal geliştiricisinin sürdürdüğü Rubocop'u veya JSLint, ESLint'i veya daha az bilinen veya daha az popüler olan bir şeyi düşünebilirsiniz.

Bu makale sizi farklı bir linter türüyle tanıştıracak. Kod sözdizimini kontrol etmezler veya Özet-Sözdizimi Ağacını doğrulamazlar, ancak kodu doğrularlar. Bir uygulamanın belirli bir arabirime bağlı olup olmadığını, yalnızca sözcüksel olarak (ördek yazma ve klasik arabirimler açısından) değil, bazen anlamsal olarak da kontrol ederler.

Onlara aşina olmak için bazı pratik örnekleri analiz edelim. Hevesli bir Rails uzmanı değilseniz, önce bunu okumak isteyebilirsiniz.

Temel bir Lint ile başlayalım.

ActiveModel::Lint::Testler

Bu Lint'in davranışı, resmi Rails belgelerinde ayrıntılı olarak açıklanmıştır:

“Bir nesnenin Active Model API ile uyumlu olup olmadığını ActiveModel::Lint::Tests TestCase test edebilirsiniz. Nesnenizin tamamen uyumlu olup olmadığını veya değilse, API'nin hangi yönlerinin uygulanmadığını size söyleyen testleri içerecektir. Eylem Paketi ile çalışmak için tüm API'leri uygulamak için bir nesnenin gerekli olmadığını unutmayın. Bu modül, yalnızca tüm özellikleri kutudan çıkarmak istemeniz durumunda rehberlik sağlamayı amaçlamaktadır.”

Bu nedenle, bir sınıf uyguluyorsanız ve onu redirect_to, form_for gibi mevcut Rails işlevleriyle kullanmak istiyorsanız, birkaç yöntem uygulamanız gerekir. Bu işlevsellik, ActiveRecord nesneleri ile sınırlı değildir. Nesnelerinizle de çalışabilir, ancak düzgün bir şekilde vaklamayı öğrenmeleri gerekir.

uygulama

Uygulama nispeten basittir. Test senaryolarına dahil edilmek üzere oluşturulmuş bir modüldür. test_ ile başlayan yöntemler, çerçeveniz tarafından uygulanacaktır. @model örnek değişkeninin testten önce kullanıcı tarafından ayarlanması beklenir:

 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

kullanım

 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::Serileştirici::Lint::Testler

Aktif model serileştiriciler yeni değil, ancak onlardan öğrenmeye devam edebiliriz. Bir nesnenin Active Model Serializers API ile uyumlu olup olmadığını doğrulamak için ActiveModel::Serializer::Lint::Tests dahil edersiniz. Değilse, testler hangi parçaların eksik olduğunu gösterecektir.

Ancak, belgelerde anlambilimi kontrol etmediğine dair önemli bir uyarı bulacaksınız:

“Bu testler, döndürülen değerlerin anlamsal doğruluğunu belirlemeye çalışmaz. Örneğin, her zaman {} döndürmek için serializable_hash uygulayabilirsiniz ve testler başarılı olur. Değerlerin anlamsal olarak anlamlı olmasını sağlamak size kalmış.”

Başka bir deyişle, sadece arayüzün şeklini kontrol ediyoruz. Şimdi nasıl uygulandığına bakalım.

uygulama

Bu, bir an önce ActiveModel::Lint::Tests uygulamasında gördüğümüze çok benzer, ancak bazı durumlarda biraz daha katıdır çünkü döndürülen değer sınıflarını veya ariteyi kontrol eder:

 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 ...

kullanım

ActiveModelSerializers tiftiği test senaryosuna dahil ederek nasıl kullandığına dair bir örnek:

 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

Raf::Lint

Önceki örnekler anlambilimle ilgilenmiyordu.

Ancak Rack::Lint tamamen farklı bir canavar. Uygulamanızı sarabileceğiniz Rack ara katman yazılımıdır. Ara katman yazılımı bu durumda bir linter rolü oynar. Linter, isteklerin ve yanıtların Rack özelliklerine göre oluşturulup oluşturulmadığını kontrol edecektir. Bu, Rack uygulamasına hizmet edecek bir Rack sunucusu (yani Puma) uyguluyorsanız ve Rack spesifikasyonunu takip ettiğinizden emin olmak istiyorsanız kullanışlıdır.

Alternatif olarak, çok çıplak bir uygulama uyguladığınızda ve HTTP protokolü ile ilgili basit hatalar yapmadığınızdan emin olmak istediğinizde kullanılır.

uygulama

 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 ...

Uygulamanızda Kullanım

Diyelim ki çok basit bir uç nokta oluşturduk. Bazen “İçerik Yok” şeklinde yanıt vermesi gerekir, ancak kasıtlı bir hata yaptık ve vakaların %50'sinde bir miktar içerik göndereceğiz:

 # 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

Bu gibi durumlarda, Rack::Lint yanıtı yakalar, doğrular ve bir istisna oluşturur:

 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'da Kullanım

Bu örnekte Puma'nın çok basit bir aplikasyonu nasıl sardığını görüyoruz lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] } lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] } önce bir ServerLint ( Rack::Lint Lint'ten miras alır), ardından ErrorChecker .

Spesifikasyona uyulmaması durumunda tiftik istisnalar oluşturur. Denetleyici istisnaları yakalar ve 500 hata kodunu döndürür. Test kodu, istisnanın oluşmadığını doğrular:

 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'nın Raf uyumluluğu sertifikası bu şekilde doğrulanır.

RailsEventStore - Depo Lint

Rails Event Store, olayları yayınlamak, tüketmek, depolamak ve almak için bir kitaplıktır. Rails uygulamanız için Event-Driven Architecture'ı uygulamada size yardımcı olmayı amaçlar. Depo, eşleyici, gönderici, zamanlayıcı, abonelikler ve seri hale getirici gibi küçük bileşenlerle oluşturulmuş modüler bir kitaplıktır. Her bileşenin değiştirilebilir bir uygulaması olabilir.

Örneğin, varsayılan depo ActiveRecord'u kullanır ve olayları depolamak için belirli bir tablo düzenini varsayar. Ancak, uygulamanız ROM kullanabilir veya olayları depolamadan bellek içi çalışabilir, bu da test için kullanışlıdır.

Ancak uyguladığınız bileşenin kitaplığın beklediği şekilde davranıp davranmadığını nasıl bilebilirsiniz? Tabii ki sağlanan linter kullanarak. Ve muazzam. Yaklaşık 80 vakayı kapsar. Bazıları nispeten basittir:

 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

Bazıları biraz daha karmaşıktır ve mutsuz yollarla ilgilidir:

 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

Yaklaşık 1.400 satırlık Ruby kodunda, bunun Ruby'de yazılmış en büyük linter olduğuna inanıyorum. Ama daha büyüğünü biliyorsanız, bana bildirin. İlginç olan kısım, anlambilimle ilgili %100 olmasıdır.

Arayüzü de yoğun bir şekilde test ediyor, ancak bu makalenin kapsamı göz önüne alındığında bunun sonradan düşünüldüğünü söyleyebilirim.

uygulama

Depo linter, RSpec Paylaşılan Örnekler işlevi kullanılarak uygulanır:

 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 # ...

kullanım

Bu linter, diğerlerine benzer şekilde, sizden bazı yöntemler sağlamanızı, en önemlisi de uygulamanın doğrulanmasını sağlayan repository sağlamanızı bekler. Test örnekleri, yerleşik RSpec include_examples yöntemi kullanılarak dahil edilmiştir:

 RSpec.describe EventRepository do include_examples :event_repository let(:repository) { EventRepository.new(serializer: YAML) } end

Toplama

Gördüğünüz gibi, “ linter” genellikle aklımızda olandan biraz daha geniş bir anlama sahiptir. Bazı değiştirilebilir ortak çalışanlar bekleyen bir kitaplık uyguladığınızda, bir linter sağlamayı düşünmenizi öneririm.

Başlangıçta bu tür testleri geçen tek sınıf yine kütüphaneniz tarafından sağlanan bir sınıf olacak olsa bile, bir yazılım mühendisi olarak genişletilebilirliği ciddiye aldığınızın bir işaretidir. Ayrıca, kodunuzdaki her bir bileşenin arayüzü hakkında yanlışlıkla değil, bilinçli olarak düşünmenizi de zorlayacaktır.

Kaynaklar

  • ActiveModel: Herhangi Bir Ruby Nesnesini ActiveRecord Gibi Hissettirin
  • Aktif Model Hakkında RailsCast
  • Tam Raf Lint Uygulaması
  • Puma'da Raf Tiftiği Kullanımı
  • Rails Etkinlik Mağazası Hakkında Daha Fazla Bilgi Edinin