RSpec中的metadata

之前看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:[email protected]/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了。

参考

Rspec core metadata

Rspec core shared context

Effective Testing with RSpec 3

附:

这里简单说明下aggregate_failure是干嘛用的。常规情况下,当执行一组exceptions时,在执行到第一个失败的exception后程序就跳出这个example了,不会继续执行后面的exception,直接开始执行下一个example,而通过aggregate_failure,程序会执行完该组所有的exceptions,然后列出所有failures的exceptions,再去执行下一个example。