之前看shared_context那块,对使用metadata来调用shared_context不是很理解,Google下相关文档,终于整明白了,记录下metadata的定义和使用。
是什么
官网上V3.8 的rspec-core中metadata部分,含三部分current_example, described class, User-defined metadata,但是没有明确的定义metadata[也可能是我没找到……]。
tutorials point针对metadata给到的定义:
data about your describe, context and it blocks.
Effective Testing with RSpec 3 在chapter8中提到Metadata解决了RSpec中的一个特殊问题:
Keep information about the context your specs are running in
类似一个信息存储盒,里面存储了如下信息:
- Example configuration(for example, marked as skipped or pending)
- Source code locations
- Status of the previous run
- How one example runs differently than others
可以把metadata看成一个带着example group相关信息的hash对象。
Metadata中默认的keys包括:
:description, :full_description, :described_class, :file_path, :example_group, :last_run_status ……..
想要查看metadata的内容,需要通过当前的example来调用metadata这个method,而example对象的引用,可以通过 it, subject, let , before, after, around 后接block来获取。
看个简单的例子感受下。
安装Rspec后,在rails项目中随意跑一个example,比如:
## spec/metadata_spec.rb
require 'rails_helper'
RSpec.describe :hash do
it "hello" do |example|
pp example.metadata
end
end
执行后,输出如下结果:
{:block=>
#<Proc:0x007fcd83b1dc28@/Users/xxxx/xxx/spec/metadata_spec.rb:4>,
:description_args=>["hello"],
:description=>"hello",
:full_description=>"hash hello",
:described_class=>:hash,
:file_path=>"./spec/metadata_spec.rb",
:line_number=>4,
:location=>"./spec/metadata_spec.rb:4",
:absolute_file_path=>"/Users/xxxx/xxx/spec/metadata_spec.rb",
:rerun_file_path=>"./spec/metadata_spec.rb",
:scoped_id=>"1:1",
...........
:last_run_status=>"unknown"}
这里输出的都是metadata默认的内容,你也可以自定义metadata。
Rspec官网上举了一些例子,这里看一个简单的,给你的example加上tag fast:
## spec/metadata_spec.rb
require 'rails_helper'
RSpec.describe :hash do
it "hello", fast: true do |example|
pp example.metadata
end
end
## 也可以写成it "hello", :fast do |example|, Rspec默认缺省的value为true
执行后,会发现输出结果中有:fast=>true
。
如果是在RSpec.describe中添加fast,则内嵌的所有example的metadata中都会带有:fast=>true
。
使用场景
通过metadata可以给example或者example group添加同样的metadata,这样执行时,可以选择执行匹配要求的example了,此外也可以将module批量include到指定的example group中【后面会提到】。
那么,如何实现批量添加呢?
使用Rspec的config.define_derived_metadata。
看两个场景:
- 给某个目录下的所有example group都添加上同一个metadata
- 有条件地添加特定的metadata
指定目录下的所有example group都添加上同一个metadata
用过Rspec都知道,所有spec/controllers下的文件,都不需要添加type: :controller 这个metadata,RSpec会给所有该目录下的example group默认添加上type: :controller,这个是怎么实现的呢?
像这样:
### spec/spec_helper.rb
RSpec.configure do |config|
config.define_derived_metadata(file_path: /spec\/controllers/) do |meta|
meta[:type] = :controller
end
end
再比如:给spec/unit下的所有example group都添加上focus这个tag。
### spec/spec_helper.rb
RSpec.configure do |config|
config.define_derived_metadata(file_path: /spec\/unit/) do |meta|
meta[:focus] = true
end
end
有条件地添加特定的metadata
这里看一个实用性的例子,有关aggregate_failures【没用过aggregate_failures?参考部分附上了aggregate_failure的简单说明,可有助理解】。
要求除了已经添加aggregate_failures为false的example group,所有其他的example group都将aggregate_failure设置为true。
比如在clazz_a和clazz_b中,添加了aggregate_failures为false, 现在希望除了clazz_a和clazz_b外,其他都默认指定aggregate_failures为true。
### spec/models/clazz_a_spec.rb
RSpec.descirbe :clazz_a, aggregate_failures: false do
context "xxx" do
......
end
end
### spec/models/clazz_b_spec.rb
RSpec.descirbe :clazz_b, aggregate_failures: false do
context "xxx" do
......
end
end
实现的方式也很简单:
### spec/spec_helper.rb
RSpec.configure do |config|
config.define_derived_metadata do |meta|
meta[:aggregate_failures] = true unless meta.key?(:aggregate_failures)
end
end
如果example group中已经添加了aggregate_failures,则pass,否则添加aggregate_failures为true。
Cool!
back to shared_context
回到之前困惑的shared_context 中。
我们知道,通过使用include_context “xxx” 可以调用定义的shared_context,同样,也可以通过给example添加metadata来调用。
比如:
#### spec/support/shared_stuff.rb
RSpec.shared_context "shared stuff" do
let(:h) { { :hello => "world" } } ## 没啥意义的context,仅仅用来举例
end
RSpec.configure do |config|
config.include_context "shared stuff", include_shared: true
end
使用时,只要给example group添加上include_shared: true
这个metadata即可, 效果等同于调用了include_context "shared stuff"
。
#### spec/shared_stuff_example.rb
require 'rails_helper'
require "support/shared_stuff.rb"
RSpec.describe "this is an example", :include_shared do
it { expect(h[:hello]).to eq "world" }
end
执行,pass。
上面的例子,可以理解为所有添加了include_shared: true
的example group 都执行了include_context "shared stuff"
,事实上,确实如此。
再看一个更常见的例子:
我们在一个module Signin中定义了一些实现不同用户登录的methods【比如sgin_in_admin, sign_in_guest……】,现在希望在spec/controller下的所有example group中都可以调用这些method的。
使用metadata可以这样实现:
### spec/support/sign_in.rb
module SignIn
..... ### methods
end
RSpec.configure do |config|
config.include SignIn, type: :controller
end
这样,所有metadata中type为controller的example group便都会执行include SignIn
了。
参考
Effective Testing with RSpec 3
附:
这里简单说明下aggregate_failure是干嘛用的。常规情况下,当执行一组exceptions时,在执行到第一个失败的exception后程序就跳出这个example了,不会继续执行后面的exception,直接开始执行下一个example,而通过aggregate_failure,程序会执行完该组所有的exceptions,然后列出所有failures的exceptions,再去执行下一个example。