On Wednesday, February 11, 2015 at 7:23:13 PM UTC-8, Jesse Whitham wrote:
>
> So I ran into this problem with Testing our API.
>
> The problem is the get request is called multiple times based on examples. 
> e.g this code below will run get 'test' twice.
>
> require 'rails_helper'
>
> describe API::TestController, type: controller do
>   before do
>      get 'test'
>   end
>
>   it { expect(response).to be_ok }
>   it { expect(response.body).to eq('test code')end
>
> This is a problem when you start to have more expect statements in terms 
> of performance. As far as I know there is no good workarounds for examples 
> to re use the same response. The guide herehttp://betterspecs.org/#single 
> talks 
> about putting multiple expects into the it statement, this seems to go 
> against getting good failure responses.
>
> Using a before(:all) you get an error like so
>
> Failure/Error: get 'test'
>      RuntimeError:
>        @routes is nil: make sure you set it in your tests setup method.
>
> Is there a way to send only one request without ruining the failure 
> responses?
> (or if you like use memoization over multiple examples)
>
> I did find you could use a global variable but this seems like the worst 
> code ever.
>
> require 'rails_helper'
>
> describe API::TestController, type: controller do
>   it 'makes a single request' do
>     get 'test'
>     $stupid_global = response
>   end
>   it { expect($stupid_global).to be_ok }
>   it { expect($stupid_global.body).to eq('test code')end
>
>
> I posted this here https://github.com/rspec/rspec-core/issues/1876 and 
> got this response:
>
> This conundrum (shared state vs performance is one of the reasons we 
> added compound matchers to RSpec 3.2, so you can now do:
>
>
> it { expect(response).to be_ok.and eq 'test code' }
>
>
> This isn't a complete solution of course but we don't want to advocate 
> shared state across examples.
>
> Incidentally Github issues are not the place to request support, please 
> use the mailing list / google group (https:
> //groups.google.com/forum/#!forum/rspec) and/or #rspec on freenode."
>
>
> I really don't see this as a even usable solution as if you have 100 
> expectations
>
>
> And you compound those you end up with failure in one string like so:
>
>
> Failure/Error: "we expected it to have this and  and we expected it to 
> have this and we expected it to have this and we expected it to have this 
> and we expected it to have this and we expected it to have this we 
> expected it to have this we expected it to have this we expected it to 
> have this we expected it to have this we expected it to have this we 
> expected it to have this"
>
> you don't compound them have one useless string with lots of expectations 
>
> Failure/Error: "we expected the response to be ok (not sure why its not)"
>
> or you make 100 requests (massive performance load).
>
> Does anyone have any suggestions for better ways? Alternative testing 
> frameworks? (maybe rspec just isn't useful for this kind of testing) or 
> even a feature for shared state? (By the sounds of it this will not be 
> supported)
>
>

Hey Jesse,

This is a great question. One solution, which has been available for years, 
is to use a before(:context) (or before(:all) — that’s the old RSpec 2.x 
form, and it still works in RSpec 3) hook. See, for example, this PR 
<https://github.com/rspec/rspec-support/pull/179/files#diff-ec40054ce667411396ff663c4d03bb50R65>
 where 
I’m doing a slow operation in before(:context), storing it in an instance 
variable, making it available via some attr_reader declarations, and using 
the results from multiple examples.

Note, however that before(:context) hooks come with many caveats. (See the 
“Warning: before(:context)” section from our docs 
<http://rspec.info/documentation/3.2/rspec-core/RSpec/Core/Hooks.html#before-instance_method>).
 
The basic problem is that many things that integrate with RSpec — such as 
DB transactions from DB cleaner or rspec-rails, or the rspec-mocks test 
double life cycle — have a per-example life cycle, and running logic 
*outside* of that lifecycle can cause problems. If you create DB records in 
before(:context) and are using per-example DB transactions, it would create 
the records and not clean them up afterwords, potentially affecting later 
tests. So I’d say the before(:context) solution is great as long as you 
don’t have per-example life cycle stuff going on. If you do have that kind 
of stuff going on (and it’s very common to, especially in a rails context) 
you’re better off avoiding before(:context) or at least being extremely 
careful what you do in there.

I think the “one expectation per example” guideline is a useful corrective 
to a pattern many first-time testers fall into, where they do too much in 
one test or one example, and have hard-to-understand test failures, but 
it's not something I recommend following strictly. Personally, I use “one 
expectation per example” as a signal…if I’m putting multiple expectations 
in one example I may be specifying multiple behaviors. In fast, isolated 
unit tests you want to keep each example focused on one behavior. In 
slower, integrated tests that’s far less important, and the cost of the 
setup time (and different kind of test) causes me to not worry about “one 
expectation per example”. If you are doing slow integrated testing and the 
thing being is so complicated that it needs 100 expectations (as per your 
hypothetical case), that suggests to me that your logic could benefit from 
being refactored, with more of it being extracted into stand-alone ruby 
objects that don’t interact with the slow external things and can be 
quickly unit tested in isolation.

One other thing I’ve been mulling over recently is a new feature in RSpec 
that would better support what you’re trying to do. I’m thinking it would 
be something like:

it "returns a successful response" do
  get 'test'
  aggregate_failures do
    expect(response).to be_ok
    expect(response.body).to eq("test code")
  endend

The idea is that aggregate_failures (not necessarily what we’ll call it — 
it’s the best name I’ve thought of so far, though) will change how expect works 
for the duration of the block so that rather than aborting on first 
failure, it collects all expectation failures until the end of the example, 
and the block, and then, if there were any failures in the block, it’ll 
abort at that point with all of the failure output.

Would that do what you want?

HTH,
Myron 

-- 
You received this message because you are subscribed to the Google Groups 
"rspec" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/rspec/27e919c9-930f-43d0-bedd-4c7aeed4ad4a%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to