简体   繁体   English

带RSpec的DRY控制器规格

[英]DRY controller specs with RSpec

I'm currently struggling a bit trying to keep my controller specs DRY and succinct and down to one assertion per example. 我目前正在努力保持我的控制器规格DRY和简洁,并在每个示例下降为一个断言。 I'm running into some difficulties particularly with where to place the actual controller request call within a structure nested to match the various edge cases. 我遇到了一些困难,特别是在嵌套的结构中将实际的控制器请求调用放在哪里以匹配各种边缘情况。

Here's an example, simplified to demonstrate the problem: 这是一个示例,简化为演示问题:

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

Clearly this is a contrived example, but it illustrates what I'd like to do and what's not working. 显然这是一个人为的例子,但它说明了我想做什么和什么不行。 Mainly, the before block under the "unpublished" context is the problem. 主要是,“未发布”上下文中的before块是问题所在。 What happens is the change I made to the setup data actually happens after the get call due to the way the contexts are nested, so the example in that context is actually working with the initial scenario rather than the one I intend. 会发生什么事是我的设置数据实际发生进行了更改get调用由于上下文嵌套的方式,因此在这方面的例子实际上与最初的方案,而不是一个我想要的工作。

I understand why this happens and how contexts nest. 我理解为什么会发生这种情况以及上下文如何嵌套。 I guess what I'd like to have is some way to tell RSpec what I'd like it to run right after any before hooks yet right before any assertions within a given context. 我想我有一些方法来告诉RSpec的想什么,我就往右的任何运行before钩子在给定范围内的任何断言右呢。 This would be perfect for controller specs. 这对控制器规格来说是完美的。 I'd like to take advantage of nesting in my controller specs to gradually build up variations of edge cases without having to scatter the get call or even a call to a do_get helper into each of my it assertions. 我想利用我的控制器的规格,以逐步建立的边缘情况变化筑巢,而无需散射get通话,甚至打电话到do_get帮助到每一个我的it的断言。 This would especially get annoying to keep in sync with any custom it_should macros I'm using. 这与我正在使用的任何自定义it_should宏保持同步尤其令人讨厌。

Is there anything in RSpec currently to accomplish this? 目前RSpec还有什么可以实现这一目标吗? Are there any tricks I can use to get close? 有什么技巧可以用来接近吗? It would seem perfectly suited to the way I've seen a lot of people writing their controller specs; 它看起来非常适合我看到很多人编写控制器规格的方式; from what I've found, people have basically settled for having do_get helpers called before every assertion. 根据我的发现,人们基本上已经决定在每次断言之前调用do_get助手。 Is there a better way? 有没有更好的办法?

The DRY principle states that "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system." DRY原则指出“每一条知识都必须在一个系统中具有单一,明确,权威的表示。” What you're doing is much more about saving a few characters here and there than keeping things DRY, and the result is a tangled web of dependencies up and down a hierarchy that, as you can see, is a bitch to get to do what you want it to and consequently fragile and brittle. 你正在做的更多是关于在这里和那里保存一些字符,而不是保持干燥,结果是一个层层叠叠的网络上下层,正如你所看到的,是一个婊子去做什么你想要它,因此脆弱和脆弱。

Let's start with what you've got written out in a way that's verbose and works: 让我们从你用一种冗长而有效的方式写出的内容开始:

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

Now the only "pieces of knowledge" that are being duplicated are the names of the examples, which could be generated by the matchers at the end of each example. 现在,重复的唯一“知识”是示例的名称,这些名称可以由每个示例末尾的匹配器生成。 This can be resolved in a readable way by using the example method, which is an alias of it : 这可以通过使用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

There. 那里。 DRY. 干。 And quite readable and easy to change. 并且非常易读且易于更改。 Now, when you happen to add more examples for either of the contexts, you can add a let : 现在,当您碰巧为任一上下文添加更多示例时,您可以添加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

Now the only duplicated code (not the same as the DRY principle) is the get . 现在唯一重复的代码(与DRY​​原理不同)是get If you really feel strongly about it, you can delegate those calls out to a method like get_show(id) or some such, but it's not really buying much at that point. 如果你真的对它有强烈的感觉,你可以将这些调用委托给get_show(id)类的方法或者其他类似的方法,但是那时它并没有真正买多少。 It's not like the API for get is going to change from under you, and the only argument to get is the item 's id, which you actually care about in the example (so there's no unnecessary information). 它不像get的API会从你的下面改变, get的唯一参数是item的id,你在示例中真正关心的(所以没有不必要的信息)。

As for using subject to capture the response and get one-liners out of the deal, that just makes things really difficult to read and doesn't save you much. 至于使用subject来捕获响应并从交易中获得单行,这只会使事情变得非常难以阅读并且不会为您节省太多。 In fact, I've come to consider using subject in this way to be a smell . 事实上,我开始考虑以这种方式使用subject 是一种气味

Hope this all helps. 希望这一切都有帮助。

Cheers, David 干杯,大卫

Will

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

  it { should redirect_to(error_url) }
end

work for you? 为你工作? BTW, before by default is before(:each) so you can DRY you specs a little more. 顺便说一句, before默认为before(:each)这样你就可以干你的规格多一点。

UPDATE: you can also isolate examples with anonymous contexts, like: 更新:您还可以使用匿名上下文隔离示例,例如:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM