[rspec-users] Building higher-level DSLs

Sam Goldman samwgoldman at gmail.com
Thu Feb 7 23:32:57 UTC 2013


I'm not sure if this is what you are looking for, but I do have some
experience with writing DSLs on top of RSpec.

I made a project that lets you write tests for HTTP APIs in a bit more of a
straightforward manner[1]. This isn't quite the level of DSL-ness that it
seems you are going for—i.e., the specs using this library still very much
look like RSpec.

In the context of this project, I think that it was worth it. I very much
enjoy writing specs in the style afforded by this DSL.

I think the tradeoff is about test noise. Let me define noise as details
that are only relevant at a lower level of abstraction from the spec's
level of abstraction. Ideally, your DSL will only hide noise. If your DSL
hides signal, then consumers of your dialect will need to understand the
internals.

That said, I think it's a bad idea to start with a DSL. Write your tests
directly, then once you have a good number of tests, ask yourself if a
non-leaky abstraction exists would let you convey your specs at a higher
level. You might find that this leads to a new domain concept for your
production code. Alternatively it might be better to augment your tests
with a little DSL.

Sorry if that's a little too philosophical :P

Sam

1. https://github.com/smartlogic/http_spec


On Thu, Feb 7, 2013 at 8:26 AM, Ash Moran <ash.moran at patchspace.co.uk>wrote:

> Hi
>
> Someone on another mailing list I'm on recently posted asking for people's
> thoughts on test naming practices, and writing my reply made me think about
> some of the techniques I use to improve naming and remove duplication in my
> own spec files.
>
> The most worked-through example I have is the contract test for my
> solutions to the Tennis Kata[1]. (I'm not implying this is the best way to
> tackle the Tennis Kata.) Like with everything other spec suite, I started
> out using plain describe/context/it type language[2], which contains a lot
> of duplication:
>
>   describe "scoring" do
>     before(:each) { tennis.start_game }
>
>     context "with no advantages" do
>       context "A" do
>         before(:each) { tennis.point_to_player_a }
>         specify { expect(@score).to be == "15-0" }
>
>         context "A" do
>           before(:each) { tennis.point_to_player_a }
>           specify { expect(@score).to be == "30-0" }
>         end
>
>         context "B" do
>           before(:each) { tennis.point_to_player_b }
>           specify { expect(@score).to be == "15-15" }
>
> And it so goes on for quite a bit longer in the same style. The first step
> is to factor out the duplication into context helpers, which leaves code
> like this:
>
>   game_started do
>     score_is_now "0-0"
>
>     context "with no advantages" do
>       point_to_player :a do
>         score_is_now "15-0"
>
>         point_to_player :a do
>           score_is_now "30-0"
>         end
>
> The problem now is that the meaning of the specification is now hidden in
> the implementation of the helpers, in this case it was in spec_helper[3].
> This gives very poorly composed methods with mixed levels of abstraction.
> So the final step is to parameterise the spec DSL with blocks of code from
> the specs itself, allowing you to write:
>
>   specification_dsl :tennis do
>     for_context :game_not_started do
>       nothing
>     end
>
>     for_context :game_started do
>       tennis.start_game
>     end
>
>     for_context :point_to_player do |player|
>       # Heh, just noticed writing this email that I could be doing
>       # tennis.send(:"point_to_player_#{player}") here, hey ho
>       player == :a ? tennis.point_to_player_a : tennis.point_to_player_b
>     end
>
>     for_context :deuce do
>       3.times do
>         tennis.point_to_player_a
>         tennis.point_to_player_b
>       end
>     end
>
>     to_expect :score_is_now do |expected_score|
>       expect(@scores.last).to be == expected_score
>     end
>
> This has finally put the spec and its definition back together[4], with
> the DSL definition and its voodoo metaprogramming hidden away in
> spec_helper[5].
>
> Unfortunately there's a problem with this implementation, which is that it
> fools RSpec into thinking expectation failures are all coming from
> spec_helper.rb, which makes for rather useless error messages. I haven't
> investigated this.
>
> Anyway, the point of explaining this example is to ask for people's
> opinions myself. A few obvious questions are:
>
> * What sort of DSL-building have you tried/seen?
> * Is this worth the effort over e.g. helper modules and custom matchers?
> (E.g. is the terseness worth the indirection?)
> * Is this possible in a simpler way with existing context tools in RSpec?
> * If not, is it worth trying to make this DSL definition reusable?
> * Are the situations where this is useful inherently best tackled another
> way?
>
> I'm interested in any opinions though, especially if you have a valid
> reason why this is a bad idea / approach. I've sat on it long enough I'm
> clearly not going to have any more insights on my own any time soon.
>
> Cheers
> Ash
>
> [1] http://codingdojo.org/cgi-bin/wiki.pl?KataTennis
> [2]
> https://github.com/ashmoran/tennis_kata/blob/ff46b7e502337988940ef9ed881c934a04c53766/spec/tennis_spec.rb
> [3]
> https://github.com/ashmoran/tennis_kata/blob/a159bac3c3d6180c6f1b83770e6cd678fa750b33/spec/spec_helper.rb
> [4]
> https://github.com/ashmoran/tennis_kata/blob/master/spec/tennis_contract.rb
> [5]
> https://github.com/ashmoran/tennis_kata/blob/master/spec/spec_helper.rb
>
> --
> http://www.patchspace.co.uk/
> http://www.linkedin.com/in/ashmoran
>
> _______________________________________________
> rspec-users mailing list
> rspec-users at rubyforge.org
> http://rubyforge.org/mailman/listinfo/rspec-users
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://rubyforge.org/pipermail/rspec-users/attachments/20130207/27e26df7/attachment.html>


More information about the rspec-users mailing list