带RSpec的DRY控制器规格

时间:2012-02-13 20:01:32

标签: ruby-on-rails rspec dry functional-testing

我目前正在努力保持我的控制器规格DRY和简洁,并在每个示例下降为一个断言。我遇到了一些困难,特别是在嵌套的结构中将实际的控制器请求调用放在哪里以匹配各种边缘情况。

以下是一个示例,简化为演示问题:

describe MyController do
  let(:item) { Factory(:item) }
  subject { response }

  describe "GET #show" do
    before(:each) do
      get :show
    end

    context "published item" do
      it { should redirect_to(success_url) }
    end

    context "unpublished item" do
      before(:each) do
        item.update_attribute(published: false)
      end

      it { should redirect_to(error_url) }
    end
  end
end

显然,这是一个人为的例子,但它说明了我想做什么和什么不行。主要是,“未发布”上下文中的before块是问题所在。由于上下文嵌套的方式,我在<{em> get调用之后实际发生了对设置数据的更改所发生的变化,因此该上下文中的示例实际上是在处理初始场景而不是我想要的那个。

我理解为什么会发生这种情况以及上下文如何嵌套。我想我喜欢有什么方法告诉RSpec我希望它 任何before挂钩之后 >在给定上下文中的任何断言之前。这对控制器规格来说是完美的。我想利用我的控制器规范中的嵌套来逐渐构建边缘案例的变体,而不必将get调用或甚至调用do_get助手分散到我的每个it助手中。 1}}断言。与我正在使用的任何自定义it_should宏保持同步尤其令人讨厌。

目前RSpec有什么可以实现这一目标吗?有什么技巧可以用来接近吗?它看起来非常适合我看到很多人编写控制器规格的方式;根据我的发现,人们基本上已经决定在每次断言之前调用do_get助手。还有更好的方法吗?

2 个答案:

答案 0 :(得分:6)

DRY原则指出“每一条知识都必须在系统内具有单一,明确,权威的表现形式。”你正在做的更多是关于在这里和那里保存一些字符,而不是保持干燥,结果是一个层层叠叠的网络上下层,正如你所看到的,是一个婊子去做什么你想要它,因此脆弱和脆弱。

让我们从你用一种冗长而有效的方式写出的内容开始:

describe MyController do
  describe "GET #show" do
    context "published item" do
      it "redirects to the success url" do
        item = Factory(:item, published: true)
        get :show, :id => item.id
        response.should redirect_to success_url
      end
    end

    context "unpublished item" do
      it "redirects to the error url" do
        item = Factory(:item, published: false)
        get :show, :id => item.id
        response.should redirect_to error_url
      end
    end
  end
end

现在唯一重复的“知识片段”是示例的名称,这些名称可以由每个示例末尾的匹配器生成。可以使用example方法以可读的方式解决此问题,该方法是it的别名:

describe MyController do
  describe "GET #show" do
    context "published item" do
      example do
        item = Factory(:item, published: true)
        get :show, :id => item.id
        response.should redirect_to success_url
      end
    end

    context "unpublished item" do
      example do
        item = Factory(:item, published: false)
        get :show, :id => item.id
        response.should redirect_to error_url
      end
    end
  end
end

有。干。并且非常易读且易于更改。现在,当您碰巧为上述任一上下文添加更多示例时,您可以添加let

describe MyController do
  describe "GET #show" do
    context "published item" do
      let(:item) { Factory(:item, published: true) }
      example do
        get :show, :id => item.id
        response.should redirect_to success_url
      end

      example do
        # other example
      end
    end
    # ...
  end
end

现在唯一重复的代码(与DRY​​原则不同)是get。如果你真的对此感到强烈,你可以将这些调用委托给像get_show(id)这样的方法,或者其他一些方法,但那时候并没有真正买得多。它不像get的API会从你的下方改变,get的唯一参数是item的id,你在实例中真正关心的是(所以没有不必要的信息。)

至于使用subject来捕获响应并让单行者退出交易,这只会让事情变得非常困难并且不会为您节省太多。事实上,我开始考虑以这种方式使用subject to be a smell

希望这一切都有所帮助。

干杯, 大卫

答案 1 :(得分:3)

威尔

context "unpublished item" do
  let(:item) do
    Factory(:item, published: false)
  end

  it { should redirect_to(error_url) }
end

为你工作?顺便说一句,默认情况下beforebefore(:each),因此您可以再干一点。

更新: 您还可以使用匿名上下文隔离示例,例如:

describe "GET #show" do
  let(:show!) do
    get :show
  end

  context do
    before { show! }

    context "published item" do
      it { should redirect_to(success_url) }
    end 

    # another examples with show-before-each
  end

  context "unpublished item" do
    before do
      item.update_attribute(published: false)
      show!
    end

    it { should redirect_to(error_url) }
  end
end