How to DRY up tests for exceptions and side effects?

14 views
Skip to first unread message

Jack Royal-Gordon

unread,
Jul 10, 2020, 10:05:17 AM7/10/20
to rs...@googlegroups.com
Consider the following call to a controller that you wish to test:

get 'show', id: @event1.id

Let’s say that you want to test that the code works as expected when the Event ID is invalid (it could be that no such record exists or that the record belongs to another user, etc.). So you want to test that it raises ActiveRecord::RecordNotFound, and you also want to test that it redirects to the right place or renders the right template, and you want to test that the http status indicates failure.

The obvious way to test for an exception is something like

describe “failures” do
it “throws an exception” do
expect { get 'show', id: @event1.id }.to raise_exception(ActiveRecord::RecordNotFound)
end
end

But the obvious way to test for the side effects would be something like

describe “failures” do
before(:each) do
 get 'show', id: @event1.id
end

it “signals failure” do
expect(response).to_not be_success
end

it “renders the correct template” do
expect(response).to render_template(:index)
end
end

or, the shorter but probably less desirable (because you would get the same failure message for three different errors

it “fails appropriately” do
 get 'show', id: @event1.id

expect(response).to_not be_success

expect(response).to render_template(:index)
end

But neither of these forms mixes well with the exception checker example at the top. In the interest of DRYing my example, I don’t want to repeat the “get” command. So one thought on how to DRY it up would be the following:

it “fails appropriately” do
expect { get 'show', id: @event1.id }.to raise_error(ActiveRecord::RecordNotFound)

expect(response).to_not be_success

expect(response).to render_template(:index)
end

but, as stated before, the failure messages are not very specific. So how can I combine exception checking with side-effect checking with specific messages for each test in a way that is DRY?

Thanks for staying with this question all the way through!

Jon Rowe

unread,
Jul 12, 2020, 6:09:23 AM7/12/20
to rs...@googlegroups.com
If you use aggregate failures here (either via running the test in an `aggregate_failures do … end` block or via tagging the example with the aggregate_failures metadata, your last example should give you one error that is the composite of the individual errors, 

e.g. it should allow you to see any / all of the individual failures rather than just the first.

You can also customise expectations by passing a message with them:

 e.g. expect(..).to matcher, “message”

And you can always create fully custom composite matchers if you have more specific needs...

Hope that helps

Cheers
Jon Rowe
---------------------------

Jack Royal-Gordon

unread,
Jul 12, 2020, 6:05:26 PM7/12/20
to rs...@googlegroups.com
Jon, I think I understand what you’re talking about (did a little reading about aggrregating failures), but that’s not really the issue I’m dealing with. I’m dealing with multiple tests that should all succeed, but the syntax available is such that I’m not sure how to not repeat the same request in two different places in my code and check all of the conditions. This is more of a DSL question that a functionality question. One of the conditions requires that I wrap the request with an "expect …to raise_error” while the other test requires that I test a side effect of the request “expect(response.status).to eq 404”. Those two tests handle the request itself differently and therefore cannot be aggregated into one block where the request only appears once. The best I can do to DRY up the request is to create a Proc from it and invoke the Proc twice. What I’m looking for is something like the following:

describe “Request for invalid record should fail” do
before(:each) do
get …
end

it “should raise an error” do
expect it.to raise_error(ActiveRecord::RecordNotFound)
end

it “should redirect to the list” do
expect(response).to redirect_to(widget_index_path)
end
end

I think the best I can do is:

describe “Request for invalid record should fail” do
@req_action = Proc { get … }

it “should raise an error” do
expect {@req_action.call}t.to raise_error(ActiveRecord::RecordNotFound)
end

it “should redirect to the list” do
@req_action.call
expect(response).to redirect_to(widget_index_path)
end
end



--
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 rspec+un...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/rspec/dejalu-217-7e59b0ce-5ba2-4fe6-a3c6-fdc7b95db653%40jonrowe.co.uk.

Jon Rowe

unread,
Aug 26, 2020, 3:00:09 PM8/26/20
to rs...@googlegroups.com
Hi Jack

Sorry for the delayed response but..

let(:perform) { get … }

Works for this, performs take a block and cache the result the equivalent (ish) of

def perform
  @result ||= begin
    get …
  end
end

You could also just define a method if you don’t want caching or want to rescue exceptions etc.

Cheers
Jon Rowe
---------------------------
Reply all
Reply to author
Forward
0 new messages