Rspec basic

Rspec是Ruby社区流行的测试框架之一,另一个是Minetest。

前言

RSpec包含:

respec-core

respec-expectations

rspec-mocks

rspec-rails

rspec-support

安装了rspec后,在终端执行:rspec --version,可以看到每个部分的版本。

对于RSpec框架,Rails项目里面用的比较多的是Rspec-rails。

最佳效果:写最少的测试,测最多的东西。

基本结构:

最简单的一个场景一个例子:

describe/it:

RSpec.describe "something" do
  it "does something" do
  end
end

嵌套:

RSpec.describe "something" do
  context "in one context" do
    it "does one thing" do
    end
  end

  context "in another context" do
    it "does another thing" do
    end
  end
end

在测试前,通常需要创建测试数据,一般会在每个测试案例中单独创建,也可以借助before【hook methods】来简化,减少重复创建。before调用有两种形式:

  • before(:each, &block) - default mode
  • before(:all, &block)

使用before(:each ) 创建的测试数据,在每个测试结束后都会回滚,即每个测试里面的数据都是独立的。每次执行context前,都会重新执行一次before(:each),而before(:all) 只执行一次,被这个describe/context下的所有it公用。使用before(:all)时,建议配after(:all) 来清空数据,保证多个describe之间的独立性,同时使用before(:each) 来重新加载数据,以获取最新的数据。

比如:

before(:all) do
  @widget = Widget.create!
end

before(:each) do
  @widget.reload
end

after(:all) do
  @widget.destroy
end

Shared examples

shared_examples 是Rspec的类方法,用于定义class 或者 module的行为,shared的含义就是共享,个人的理解是把多个测试中,重合度高的地方抽出来,封装起来,类似于include module,在module中定义常用方法,每个类include 该module后,就拥有了该module的实例方法。

可以使用以下四种方式来将定义的shared_examples include到context中:

include_examples "name"      # include the examples in the current context
it_behaves_like "name"       # include the examples in a nested context
it_should_behave_like "name" # include the examples in a nested context
matching metadata            # include the examples in the current context

备注:metadata是什么?describe的部分常会看到 type: :model,这个就是RSpec metadata.【官网中举了相关的例子,还不是很懂】

看个官方的例子:

require "set"

RSpec.shared_examples "a collection" do
  let(:collection) { described_class.new([7, 2, 4]) }

  context "initialized with 3 items" do
    it "says it has three items" do
      expect(collection.size).to eq(3)
    end
  end

  describe "#include?" do
    context "with an item that is in the collection" do
      it "returns true" do
        expect(collection.include?(7)).to be_truthy
      end
    end

    context "with an item that is not in the collection" do
      it "returns false" do
        expect(collection.include?(9)).to be_falsey
      end
    end
  end
end

RSpec.describe Array do
  it_behaves_like "a collection"
end

RSpec.describe Set do
  it_behaves_like "a collection"
end

带参数的shard_example:

RSpec.shared_examples "a measurable object" do |measurement, measurement_methods|
  measurement_methods.each do |measurement_method|
    it "should return #{measurement} from ##{measurement_method}" do
      expect(subject.send(measurement_method)).to eq(measurement)
    end
  end
end

RSpec.describe Array, "with 3 items" do
  subject { [1, 2, 3] }
  it_should_behave_like "a measurable object", 3, [:size, :length]
end

RSpec.describe String, "of 6 characters" do
  subject { "FooBar" }
  it_should_behave_like "a measurable object", 6, [:size, :length]
end

也可以在context中定义shared_examples,然后在该context中调用,注意,可将context看作是一个作用域,出了这个context,则不能调用定义在该context 中的shared_examples:

RSpec.describe "shared examples" do
  context "per context" do

    shared_examples "shared examples are nestable" do
      it { expect(true).to eq true }
    end

    it_behaves_like "shared examples are nestable"
  end
end

如果在另一个context中调用,则会报错:

RSpec.describe "shared examples" do
  context "per context" do

    shared_examples "shared examples are nestable" do
      it { expect(true).to eq true }
    end
  end

  context "another context" do
    it_behaves_like "shared examples are nestable"
  end
end

Shared context

shared_context顾名思义,将多个context中共用的部分抽出来,定义成method来反复调用。

调用已定义好的shared_context的方式有两种,一是include_context,而是metadata。

看一段简单的例子,先定义一个shared_context:

### one describe
RSpec.describe "somethings 1" do
 context "context 1" do
   let(:a) { create :a }
   let(:b) { create :b }
   let(:c) { create :c }
   let(:d) { create :d }
   ........ # do something
 end
end

### another describe

RSpec.describe "somethings 2" do
 context "context 2" do
   let(:a) { create :a }
   let(:b) { create :b }
   let(:c) { create :c }
   let(:d) { create :d }
   ........ # do something
 end
end

使用shared_context简化,定义shared_context :

在spec/support下定义一个文件,比如shared_context.rb:

RSpec.shared_context "shared data" do
  let(:a) { create :a }
  let(:b) { create :b }
  let(:c) { create :c }
  let(:d) { create :d }
end

简化之前的测试:

### one describe
RSpec.describe "somethings 1" do
  context "context 1" do
    include_context "shared data"
    ........ # do something
  end
end

### another describe

RSpec.describe "somethings 2" do
  context "context 2" do
    include_context "shared data"
    ........ # do something
  end
end

如果使用metadata,如何调用?参考官网的例子,需要修改下刚刚创建的这个shared_data.rb文件:

RSpec.configure do |rspec|
  rspec.shared_context_metadata_behavior = :apply_to_host_groups ## 用于设置metadata调用后的作用域
end

RSpec.shared_context "shared data", :shared_context => :metadata do
  let(:a) { create :a }
  let(:b) { create :b }
  let(:c) { create :c }
  let(:d) { create :d }
end

RSpec.configure do |rspec|
  rspec.include_context "shared data", :include_shared => true
end

那么上面的例子,就可以这样调用了:

### one describe
RSpec.describe "somethings 1", :include_shared => true do
  context "context 1" do
    ........ # do something
  end
end

### another describe

RSpec.describe "somethings 2", :include_shared => true do
  context "context 2" do
    ........ # do something
  end
end

看到这里,说一下shared_context与shared_example的区别,一个是context层面,一个是class/module层的,如果多个describe中,含有重合度高的context/describe,那么用shared_example来抽出共同的行为进行简化,如果是在多个context中,含有重合度高的代码,比如新建各种测试数据等,更偏向setup,则使用shared_context。

其它

  • 测试数据重置:

    使用gem:database_rewinder

    配置rewinder:

    # spec/support/rspec_helper.rb
    RSpec.configure do |config|
      config.before :suite do
        DatabaseRewinder.clean_all
      end
    
      config.after :each do
        DatabaseRewinder.clean
      end
    end
    
  • Shoulda-matchers

    使用Shoulda-matchers 来测试关联和验证:

    require 'rails-helper'
    
    descirbe User do
      it { should has_many(:posts).dependent(:destroy) }
      it { should has_many(:groups).through(:groupships) }
      it { should validate_presence_of :name }
    end
    
  • is_expected vs expect

    is_expected 等价于expect(subject)

    比如下面这个例子:

    it { is_expected.to validate_presence_of(:title) }
    
    # equals to
    
    it { expect(subject).to validate_presence_of(:title) }
    

    RSpec测试缺省都有一个subject,值为described_class.new,而descirbed_class 是对应需要测试的class的实例,可以当成ruby中的self,如果是model,则是model.new, controller,则是controller.new。

参考

rspec-core v 3.8

注:代码部分基本取自官网。