作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Robert是一名软件架构师,专门研究大型单片Rails应用程序. 他撰写了五本关于Rails、React和领域驱动设计的书.
当你听到"linter” or “lint你可能已经对这样一个工具是如何工作的或者它应该做什么有了一定的期望.
你可能会想到 Rubocop, which Toptal的一位开发人员表示, or of JSLint, ESLint或者不太为人所知或不太受欢迎的东西.
本文将向您介绍另一种类型的穿线器. 它们不检查代码语法,也不验证抽象语法树,但它们确实验证代码. 它们检查实现是否遵循某个接口, 不仅在词法上(就duck类型和经典接口而言)如此,有时在语义上也是如此.
为了熟悉它们,让我们分析一些实际的例子. 如果你不是狂热爱好者 Rails专业,你可能想读一下 this first.
让我们从一个基本的Lint开始.
这个Lint的行为在 官方Rails文档:
你可以测试一个对象是否与活动模型API兼容,包括 ActiveModel:线头:测试
in your TestCase
. 它将包括测试,告诉您对象是否完全兼容, if not, API的哪些方面没有实现. 注意,一个对象并不需要实现所有api才能与Action Pack一起工作. 本模块仅在您想要所有功能开箱即用的情况下提供指导.”
So, 如果您正在实现一个类,并且希望将它与现有的Rails功能一起使用,例如 redirect_to, form_for
,你需要实现一些方法. 此功能不限于 ActiveRecord
objects. 它也可以用在你的对象上,但它们需要学会正确地嘎嘎叫.
实现相对简单. 它是一个创建用于包含在测试用例中的模块. 方法开始于 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
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
主动模型序列化器并不新鲜,但我们可以继续向它们学习. You include ActiveModel::序列化器::线头::测试
验证对象是否符合 活动模型序列化器API. 如果不是,测试将指出缺少哪些部件.
However, in the docs,你会发现一个重要的警告,它不检查语义:
这些测试不试图确定返回值的语义正确性. 例如,您可以实现 serializable_hash
to always return {}
测试就会通过. 由您来确保这些值在语义上是有意义的.”
换句话说,我们只是在检查界面的形状. 现在让我们看看它是如何实现的.
这非常类似于我们刚才看到的 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}.ab = 1或-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
...
这里有一个例子 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
前面的例子不关心 semantics.
However, Rack::Lint
是一个完全不同的野兽吗. 您可以将应用程序封装在机架中间件中. 在这种情况下,中间件扮演了过滤器的角色. 过滤器将检查请求和响应是否根据Rack规范构造. 如果您正在实现机架服务器(例如机架服务器),这将非常有用.e., Puma)将服务于Rack应用程序,并且您希望确保遵循Rack规范.
Alternatively, 当您实现一个非常简单的应用程序,并且希望确保不会犯与HTTP协议相关的简单错误时,可以使用它.
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
...
假设我们建立一个非常简单的端点. 有时它应该回应“无内容”,“但我们犯了一个故意的错误,我们将在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'
在这个例子中,我们看到Puma如何包装一个非常简单的应用程序 lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] }
first in a ServerLint
(继承自 Rack::Lint
) then in ErrorChecker
.
如果没有遵循规范,lint会引发异常. 检查器捕获异常并返回错误代码500. 测试代码验证异常是否没有发生:
class TestRackServer < Minitest::Test
类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
这就是彪马如何被认证为机架兼容.
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%是关于语义的.
它也会对界面进行大量测试, 但我想说的是,考虑到本文的范围,这是一个事后的想法.
控件实现存储库筛选器 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
# ...
这个过滤器与其他过滤器类似,希望您提供一些方法,最重要的是 repository
,返回要验证的实现. 测试示例使用内置的RSpec include_examples
method:
RSpec.描述EventRepository
include_examples: event_repository
let(:repository) {EventRepository.new(serializer: YAML)}
end
As you can see, “linter” 比我们通常想到的意思稍微宽泛一点. 任何时候你实现一个库,需要一些可互换的合作者, 我建议你考虑提供一个衬垫.
即使在开始时唯一通过此类测试的类也是库提供的类, 这是一个信号,表明你作为一个软件工程师是认真对待可扩展性的. 它还要求您考虑代码中每个组件的接口, 不是偶然而是有意识的.
Lint是一个检查C源程序的命令,检测许多错误和模糊. 这就是这个词的由来.
它是指根据附加的检测规则来验证其正确性.
检查规则是检查被分析代码的特定属性. 这些可能是肤浅的, 关注风格指南, or deep, 对代码执行复杂的静态分析并查找重要的错误.
世界级的文章,每周发一次.
世界级的文章,每周发一次.
Join the Toptal® community.