作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Robert Pankowecki

Robert Pankowecki

后端架构师

Robert是一名软件架构师,专门研究大型单片Rails应用程序. 他撰写了五本关于Rails、React和领域驱动设计的书.

Share

当你听到"linter” or “lint你可能已经对这样一个工具是如何工作的或者它应该做什么有了一定的期望.

你可能会想到 Rubocop, which Toptal的一位开发人员表示, or of JSLint, ESLint或者不太为人所知或不太受欢迎的东西.

本文将向您介绍另一种类型的穿线器. 它们不检查代码语法,也不验证抽象语法树,但它们确实验证代码. 它们检查实现是否遵循某个接口, 不仅在词法上(就duck类型和经典接口而言)如此,有时在语义上也是如此.

为了熟悉它们,让我们分析一些实际的例子. 如果你不是狂热爱好者 Rails professional,你可能想读一下 this first.

让我们从一个基本的Lint开始.

ActiveModel:线头:测试

这个Lint的行为在 官方Rails文档:

你可以测试一个对象是否与活动模型API兼容,包括 ActiveModel:线头:测试 in your TestCase. 它将包括测试,告诉您对象是否完全兼容, if not, API的哪些方面没有实现. 注意,一个对象并不需要实现所有api才能与Action Pack一起工作. 本模块仅在您想要所有功能开箱即用的情况下提供指导.”

So, 如果您正在实现一个类,并且希望将它与现有的Rails功能一起使用,例如 redirect_to, form_for,你需要实现一些方法. 此功能不限于 ActiveRecord objects. 它也可以用在你的对象上,但它们需要学会正确地嘎嘎叫.

Implementation

实现相对简单. 它是一个创建用于包含在测试用例中的模块. 方法开始于 test_ 将由您的框架实现吗. 预计 @model 实例变量将由用户在测试前设置:

模块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应该返回nil?` 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应该返回nil?` returns false"
      end

      ...

      private

      def model
        Assert_respond_to @model
        @model.to_model
      end

Usage

class Person
  def persisted?
    false
  end

  def to_key
    nil
  end

  def to_param
    nil
  end

  # ...
end
#测试/模型/ person_test.rb
需要“test_helper”

class PersonTest < ActiveSupport::TestCase
  包括ActiveModel::线头::测试

  setup do
    @model = Person.new
  end
end

ActiveModel::序列化器::线头::测试

主动模型序列化器并不新鲜,但我们可以继续向它们学习. You include ActiveModel::序列化器::线头::测试 验证对象是否符合 活动模型序列化器API. 如果不是,测试将指出缺少哪些部件.

However, in the docs,你会发现一个重要的警告,它不检查语义:

这些测试不试图确定返回值的语义正确性. 例如,您可以实现 serializable_hash to always return {}测试就会通过. 由您来确保这些值在语义上是有意义的.”

换句话说,我们只是在检查界面的形状. 现在让我们看看它是如何实现的.

Implementation

这非常类似于我们刚才看到的 ActiveModel:线头:测试,但在某些情况下会更严格一些,因为它会检查返回值的大小或类别:

模块ActiveModel
  class Serializer
    module Lint
      module Tests
        # Passes if the object responds to read_attribute_for_serialization
        如果它需要一个参数(要读取的属性).
        # Fails otherwise.
        #
        # read_attribute_for_serialization gets the attribute value for serialization
        #通常,它是通过包含ActiveModel::Serialization实现的.
        def test_read_attribute_for_serialization
          assert_respond_to资源, : read_attribute_for_serialization, '资源应该响应read_attribute_for_serialization'
          Actual_arity = resource.方法: read_attribute_for_serialization).arity
          #使用绝对值,因为arity是:
          #  1 for def read_attribute_for_serialization(name); end
          # -1别名: read_attribute_for_serialization:发送
          assert_equals 1, actual_arity.预期的#{actual_arity.inspect}.abs to be 1 or -1"
        end

        # Passes if the object's class responds to model_name and if it
        #在+ActiveModel::Name+的实例中.
        # Fails otherwise.
        #
        # model_name returns an ActiveModel::Name instance.
        序列化器使用它来标识对象的类型.
        #除非启用了缓存,否则不需要.
        def test_model_name
          Resource_class = resource.class
          Assert_respond_to resource_class, model_name
          assert_instance_of resource_class.model_name, ActiveModel::名字
        end

        ...

Usage

这里有一个例子 ActiveModelSerializers 通过在它的测试用例中包含它来使用lint:

模块ActiveModelSerializers
  class ModelTest < ActiveSupport::TestCase
    包括ActiveModel::序列化器::线头::测试

    setup do
      @resource = ActiveModelSerializers::Model.new
    end

    def test_initialization_with_string_keys
      klass = Class.新(ActiveModelSerializers:模型)
        attributes :key
      end
      value = 'value'

      Model_instance = class.new('key' => value)

      assert_equal model_instance.read_attribute_for_serialization(关键),价值
    end

Rack::Lint

前面的例子不关心 semantics.

However, Rack::Lint 是一个完全不同的野兽吗. 您可以将应用程序封装在机架中间件中. 在这种情况下,中间件扮演了过滤器的角色. 过滤器将检查请求和响应是否根据Rack规范构造. 如果您正在实现机架服务器(例如机架服务器),这将非常有用.e., Puma)将服务于Rack应用程序,并且您希望确保遵循Rack规范.

Alternatively, 当您实现一个非常简单的应用程序,并且希望确保不会犯与HTTP协议相关的简单错误时,可以使用它.

Implementation

module Rack
  class Lint
    def初始化(应用)
      @app = app
      @content_length = nil
    end

    Def call(env = nil)
      dup._call(env)
    end

    def _call(env)
      引发linror, "No env given",除非env
      check_env env

      env[RACK_INPUT] = InputWrapper.新(env [RACK_INPUT])
      env[RACK_ERRORS] = ErrorWrapper.新(env [RACK_ERRORS])

      ary = @app.call(env)
      ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `.Class}”,除非任何.kind_of? Array
      ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `.Size}元素,而不是3"除非ary.size == 3

      状态,标题,@body = ary
      check_status状态
      check_headers头

      Hijack_proc = check_hijack_response headers
      if hijack_proc && headers.is_a?(Hash)
        headers[RACK_HIJACK] = hijack_proc
      end

      Check_content_type status,报头
      Check_content_length状态
      @head_request = env[REQUEST_METHOD] == HEAD
      [status, headers, self]
    end

    ## === Content-Type
    Def check_content_type(状态,报头)
      headers.每个{|键,值|
        ## There must not be a Content-Type, when the +Status+ is 1xx, 204 or 304.
        if key.Downcase == "content-type"
          如果架::跑龙套::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
            抛出LintError, "在#{status}响应中发现内容类型标头,不允许"
          end
          return
        end
      }
    end

    ## === Content-Length
    Def check_content_length(状态,报头)
      headers.每个{|键,值|
        if key.Downcase == 'content-length'
          ## There must not be a Content-Length header when the +Status+ is 1xx, 204 or 304.
          如果架::跑龙套::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
            抛出LintError, "在#{status}响应中发现内容长度头,不允许"
          end
          @content_length = value
        end
      }
    end

    ...

Usage in Your App

假设我们建立一个非常简单的端点. 有时它应该回应“无内容”,“但我们犯了一个故意的错误,我们将在50%的情况下发送一些内容:

# foo.rb
#运行与堆栈foo.rb
Foo = Rack::Builder.new do
  use Rack::Lint
  使用架::ContentLength
  App = proc do |env|
    if rand > 0.5
      no_content = Rack::Utils::HTTP_STATUS_CODES . no_content = Rack::Utils::HTTP_STATUS_CODES.转化(没有内容)
      [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

In such cases, Rack::Lint 将拦截响应,验证它,并引发异常:

Rack::Lint::LintError:在204响应中找到内容类型标头,不允许
    /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.在' block in check_content_type'

Usage in Puma

在这个例子中,我们看到Puma如何包装一个非常简单的应用程序 lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] } first in a ServerLint (继承自 Rack::Lint) then in ErrorChecker.

如果没有遵循规范,lint会引发异常. 检查器捕获异常并返回错误代码500. 测试代码验证异常是否没有发生:

class TestRackServer < Minitest::Test
  class ErrorChecker
    def初始化(应用)
      @app = app
      @exception = nil
    end

    Attr_reader:exception,:env

    def call(env)
      begin
        @app.call(env)
      rescue Exception => e
        @exception = e
        [500,{},["检测到错误"]]
      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::服务器.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

    打击((" # {@tcp} /测试"))

    stop

    refute @checker.“检查器引发异常”
  end

这就是彪马如何被认证为机架兼容.

railsevenstore -存储库Lint

Rails Event Store 库是用于发布、消费、存储和检索事件的吗. 它旨在帮助您为Rails应用程序实现事件驱动架构. 它是一个模块化库,由诸如存储库之类的小组件构建而成, mapper, dispatcher, scheduler, subscriptions, and serializer. 每个组件都可以有一个可互换的实现.

For example, 默认存储库使用ActiveRecord,并采用特定的表布局来存储事件. 但是,您的实现可以使用ROM或工作 in-memory 不存储事件,这对测试很有用.

但是,您如何知道您实现的组件是否按照库所期望的方式运行呢? By using 提供的衬垫, of course. And it’s immense. 它涵盖了大约80个案例. 其中一些相对简单:

指定'将初始事件添加到新流' do
  repository.append_to_stream([event = SRecord ..New], stream, version_none)
  期望(read_events_forward(库).first).to eq(event)
  期望(read_events_forward(存储库,流).first).to eq(event)
  期望(read_events_forward(存储库,stream_other)).to be_empty
end

还有一些更复杂,更相关 unhappy paths:

它“不允许在一个流中链接同一个事件两次”
  repository.append_to_stream(
    [SRecord.新(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.raise_error (EventDuplicatedInStream)
end

At almost 1400行Ruby代码,我相信这是用Ruby编写的最大的脚本. 但如果你知道一个更大的, let me know. 有趣的是,它100%是关于语义的.

它也会对界面进行大量测试, 但我想说的是,考虑到本文的范围,这是一个事后的想法.

Implementation

控件实现存储库筛选器 RSpec共享示例 functionality:

模块RubyEventStore
  ::RSpec.share_examples:event_repository do
    let(:helper) {EventRepositoryHelper.new }
    let(:specification){规格.新(SpecificationReader.新(存储库,映射器::NullMapper.new)) }
    let(:global_stream){流.新(GLOBAL_STREAM)}
    let(:stream){流.new(SecureRandom.uuid) }
    let(:stream_flow){流.new('flow') }

    # ...

    它'刚刚创建的是空'做
      期望(read_events_forward(库)).to be_empty
    end

    指定'append_to_stream返回self'
      repository
        .append_to_stream([event = SRecord ..New], stream, version_none)
        .append_to_stream([event = SRecord ..New], stream, version_0)
    end

    # ...

Usage

这个过滤器与其他过滤器类似,希望您提供一些方法,最重要的是 repository,返回要验证的实现. 测试示例使用内置的RSpec include_examples method:

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

Wrapping Up

As you can see, “linter” 比我们通常想到的意思稍微宽泛一点. 任何时候你实现一个库,需要一些可互换的合作者, 我建议你考虑提供一个衬垫.

即使在开始时唯一通过此类测试的类也是库提供的类, 这是一个信号,表明你作为一个软件工程师是认真对待可扩展性的. 它还要求您考虑代码中每个组件的接口, 不是偶然而是有意识的.

Resources

了解基本知识

  • 为什么linter被称为linter?

    Lint是一个检查C源程序的命令,检测许多错误和模糊. 这就是这个词的由来.

  • 什么是lint代码?

    它是指根据附加的检测规则来验证其正确性.

  • 什么是限制规则?

    检查规则是检查被分析代码的特定属性. 这些可能是肤浅的, 关注风格指南, or deep, 对代码执行复杂的静态分析并查找重要的错误.

聘请Toptal这方面的专家.
Hire Now

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 privacy policy.

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 privacy policy.

Toptal Developers

Join the Toptal® community.