[rspec-users] Possible improvements to routing spec API

David Chelimsky dchelimsky at gmail.com
Mon Jul 5 12:18:00 EDT 2010


On Jul 5, 2010, at 9:04 AM, Wincent Colaiuta wrote:

> El 05/07/2010, a las 13:56, David Chelimsky escribió:
> 
>> Nice overall. Much of the code belongs in Rails, though, so I'd like to try to get a patch in to Rails once we get this worked out. I'd like the rspec-rails matchers to be simple wrappers for rails' assertions wherever possible.
> 
> 
> Well, I'm not sure if there is a problem with the Rails assertions.

Not that there is a problem with them, but all Rails users could benefit from methods like get(path) in their routing tests. Although, now that I think of it, those assertions are available in functional tests, and the http verbs are already defined there.

The thing that concerns me the most is the DestinationParser. Even though it seems simple, that's the sort of code that ends up making rspec-rails a rails-dependent maintenance problem.

> In my (albeit limited) experience I haven't yet seen a situation where they couldn't express something during testing. So as you point out, the job of RSpec should just be to expose those APIs within specs in a usable and effective manner. If we discover some limitation that we can't get around, then sure, the upstream assertions will need some work.
> 
>> I think having three separate matchers would land us in a good spot.
> 
> Agreed. That's why I added a matcher for each of assert_recognizes, assert_generates, and assert_routing (although so far in practice I've only had to use two of them).
> 
>> The wrapped rails assertions also accept a hash representing query params, so let's see if we can accommodate that as well:
>> 
>> get('/something', :ref_id => '1234').should route_to(....)
> 
> Yes, in my initial implementation I left that out seeing as I haven't yet run into a spec that needed it, but I did have it in mind when I wrote the "get" etc methods (in fact, when I first typed them in I started with 'def get path, options = {}', but then thought, YAGNI, better not add that in until I am sure I need it...)
> 
>> Something like:
>> 
>> describe "The router" do
>>   it { should recognize(get('/wiki/foo')).as("wiki#show", :id => 'foo') }
>>   it { should generate(get('/wiki/foo')).from("wiki#show", :id => 'foo') }
>>   it { should route(get('/wiki/foo')).to("wiki#show", :id => 'foo') }
>> end
> 
> Yes, the "action#controller" notation is a good idea. I actually added that to the gist that I posted already.

That's where I got it from :)

> Not sure about nesting the get() method inside the matcher though. It seems that in most other places, we use hashes or literal values; eg:
> 
>  something.should do_something(:when => 'always', :how => 'quickly')
> 
> So, it would be more consistent with existing style to write:
> 
>  it { should recognize(:get => '/wiki/foo').as('wiki#show', :id => 'foo') }
> 
> But then we lose some of the conciseness I was looking for in my original proposal.

Keep in mind this is for a one-liner (no string passed to it), so it's still far more concise than the previous alternatives.

> In any case we also lose the similarity with controller specs (where "get" is the first thing on the line) which I was also looking for.
> 
> Nevertheless, your choice of verbs and prepositions work well together; "recognize as", "generate from" and "route to" all read fairly well.
> 
> Just to clarify, are you suggesting that:
> 
> - "recognize as" wrap assert_recognizes?
> - "generate from" wrap assert_generates?
> - "route to" wrap assert_routing?
> 
> If so, one misgiving I have is that we still have a unidirectional preposition ("to") being used in conjunction with the bidirectional assertion.

Agreed. Not sure what the right verbiage is there.

> An interesting consequence of the language you propose is that the focus of testing changes ie. in the current generator templates, we have:
> 
>  describe FooController do
>    describe 'routing' do
>      it 'recognizes and generates #index' do
>        { :get => 'bar' }.should route_to(:controller => 'bar', :action => 'index')
> 
> So the focus is on controller actions, and the subject of each 'it' block is a literal hash representing a request to that controller and action.
> 
> The new language puts the router at the center and the implicit subject of each 'it' block is really the router itself.

I think that's where it belongs. It's the router that does the routing, not the controller.

>> That's definitely nice and clean, and it makes it easier to align the names with the rails assertions, which I think adds some value. The output could be formatted something like this:
>> 
>> The router
>>   should recognize get('/wiki/foo') as wiki#show with :id => 'foo'
>>   should generate get('/wiki/foo') from wiki#show with :id => 'foo'
>>   should route get('/wiki/foo') to wiki#show with :id => 'foo'
>> 
>> Thoughts?
> 
> I think it would be nicer to format that as "GET /wiki/foo" (ie. HTTP verb followed by path) rather than embedding the literal text of the "get('/wiki/foo')" method call.

You could still get that by adding a message:

  it { should recognize(get('/wiki/foo')).as("wiki#show", :id => 'foo'), "recognizes GET /wiki/:id" }

Not saying that's any better than the other formats, just that it exists as an option.

Slight tangent - one nice thing about 'recognize' as a matcher name is we get this for free:

  it { should_not recognize(:get => '/wiki/foo') }

> In any case, I converted another RESTful resource over to the syntax I mentioned earlier, now using the "controller#action" syntax:
> 
>  describe IssuesController do
>    describe 'routing' do
>      example 'GET /issues' do
>        get('/issues').should map('issues#index')
>      end
> 
>      example 'GET /issues/new' do
>        get('/issues/new').should map('issues#new')
>      end
> 
>      example 'GET /issues/:id' do
>        get('/issues/123').should map('issues#show', :id => '123')
>      end
> 
>      example 'GET /issues/:id/edit' do
>        get('/issues/123/edit').should map('issues#edit', :id => '123')
>      end
> 
>      example 'PUT /issues/:id' do
>        put('/issues/123').should map('issues#update', :id => '123')
>      end
> 
>      example 'DELETE /issues/:id' do
>        delete('/issues/123').should map('issues#destroy', :id => '123')
>      end
>    end
>  end
> 
> Which, if you really care about conciseness, can be written as:
> 
>  describe IssuesController do
>    describe 'routing' do
>      it { post('/issues').should map('issues#create') }
>      it { get('/issues').should map('issues#index') }
>      it { get('/issues/new').should map('issues#new') }
>      it { get('/issues/123').should map('issues#show', :id => '123') }
>      it { get('/issues/123/edit').should map('issues#edit', :id => '123') }
>      it { put('/issues/123').should map('issues#update', :id => '123') }
>      it { delete('/issues/123').should map('issues#destroy', :id => '123') }
>    end
>  end

The point of "it' is that it reads as part of a sentence:

describe "something" do
  it "does something"

When we introduced the implicit subject, we got this:

describe "something" do
  it { should do_something }

In that form it still reads nicely: "it should do something".

In this case, saying "it get post issues should map issues#create" makes me want to cry. We've still got example and specify. In this case, I think specify is the most accurate:

 describe IssuesController do
   describe 'routing' do
     specify { post('/issues').should map('issues#create') }

Out loud this is "specify [that a] post [to] /issues should map [to] issues#create". I can live with that tear-free. Except when Brazil gets knocked out of the cup, but that's a different matter.

> These get formatted in the spec output as:
> 
>  IssuesController
>    routing
>      GET /issues
>      GET /issues/new
>      GET /issues/:id
>      GET /issues/:id/edit
>      PUT /issues/:id
>      DELETE /issues/:id
> 
> And:
> 
>  IssuesController
>    routing
>      should map "issues#create"
>      should map "issues#index"
>      should map "issues#new"
>      should map "issues#show" and {:id=>"123"}
>      should map "issues#edit" and {:id=>"123"}
>      should map "issues#update" and {:id=>"123"}
>      should map "issues#destroy" and {:id=>"123"}
> 
> Respectively. By adding a description method to the matcher (see updated Gist: http://gist.github.com/464081), we can produce:
> 
>  IssuesController
>    routing
>      should map POST /issues as issues#create
>      should map GET /issues as issues#index
>      should map GET /issues/new as issues#new
>      should map GET /issues/123 as issues#show with {:id=>"123"}
>      should map GET /issues/123/edit as issues#edit with {:id=>"123"}
>      should map PUT /issues/123 as issues#update with {:id=>"123"}
>      should map DELETE /issues/123 as issues#destroy with {:id=>"123"}
> 
> Which IMO looks pretty nice.

It does, though seeing that last output makes me wonder about the "map/as" verbiage. Seems less clear in this context for some reason.

Anybody else besides me and Wincent and Matt want to weigh in here?

Cheers,
David


More information about the rspec-users mailing list