Ah, OK. I'm beginning to understand the problem I was having with the macro implementation then. i was trying to pass in instance variables from my before block as parameters to the macro, but they wouldn't be available until the example inside the macro is run. That makes sense then.
I think I'll try out the shared_examples implementation. That seems like it is a nice balance of being reusable, and being descriptive as a whole as well as descriptive in the sense of using smaller chunks to describe specific behavior. Thanks for helping me understand this, David. On Wed, Dec 5, 2012 at 10:11 PM, David Chelimsky <[email protected]>wrote: > On Wed, Dec 5, 2012 at 8:05 PM, Chris Bloom <[email protected]> wrote: > > BTW: Thank you for your feedback so far! > > > > > > On Wed, Dec 5, 2012 at 9:04 PM, Chris Bloom <[email protected]> > wrote: > >> > >> I like the idea of breaking it out into small sub-tests, but I think > both > >> of the issues I mentioned previously would still be present. That is, it > >> doesn't appear that the before block is executed for macros, > > If your macro generates examples (unlike your first email this thread) > the before hooks will be run before the examples are run. > > >> so instance > >> variables from my setup code aren't available, and if I write it as a > >> matcher I still won't get the correct test result messages. I need to > test > >> this behavior for an indeterminate number of API endpoints, which is > why I > >> went with a macro in the first place. > > You can wrap my previous suggestion in a shared example group and get > something closer to what you're looking for: > > shared_examples "minimum params" do |*args| > url = description > > it "is valid with min params" do > get url, args.inject({}) {|h, k| h.merge(k => "")} > expect(response.status).to eq(200) > expect(response.body).not_to include("missing parameter:") > end > > args.each do |p| > it "requires #{p} in params" do > get url, (args - [p]).inject({}) {|h, k| h.merge(k => "")} > expect(response.status).to eq(400) > expect(response.body).to include("missing parameter: #{p}") > end > end > end > > describe "API" do > describe "/api/v1/foo" do > include_examples "minimum params", "k1", "k2" > end > > describe "/api/v1/bar" do > include_examples "minimum params", "k3", "k4", "k5", "k6" > end > end > > This outputs as follows: > > $ rspec example_spec.rb -cfd > > API > /api/v1/foo > is valid with min params > requires k1 in params > requires k2 in params > /api/v1/bar > is valid with min params > requires k3 in params > requires k4 in params > requires k5 in params > requires k6 in params > > WDYT? > > >> > >> > >> On Tue, Dec 4, 2012 at 4:21 PM, David Chelimsky <[email protected]> > >> wrote: > >>> > >>> Didn't realize you were trying to do so much in one statement. > >>> > >>> The idea of a matcher, custom or built-in, is that the match block has > >>> one expectation expressed as a boolean - it should return true or > >>> false to indicate pass or fail. This one matcher is wrapping 10 > >>> different expectations in logical pairs. I'd probably start with 5 > >>> examples with two expectations in each: > >>> > >>> it "is valid with the minimum params" do > >>> get "/api/v1/originator/hello", @minimum_params > >>> expect(response.status).to eq(200) > >>> expect(response.body).not_to include("missing parameter:") > >>> end > >>> > >>> it "requires api_key in params" do > >>> get "/api/v1/originator/hello", @minimum_params.except("api_key") > >>> expect(response.status).to eq(400) > >>> expect(response.body).to include("missing parameter: api_key") > >>> end > >>> > >>> # 3 more failure cases > >>> > >>> Each example has two expectations, but they work together to specify > >>> different parts of the same outcome, so I'm comfortable bypassing the > >>> one-expectation-per-example guideline. > >>> > >>> You could, conceivably, reduce some of the duplication with a custom > >>> matcher that just deals with one parameter - something like: > >>> > >>> it { should require_param("api_key") } > >>> > >>> Either that or wrap the failure examples in an an iterator: > >>> > >>> describe "minimum params" do > >>> MIN_PARAMS = { > >>> api_key: "", > >>> nonce: "", > >>> timestamp: "", > >>> hmac_digest: " > >>> } > >>> > >>> MIN_PARAMS.each_pair do |k, v| > >>> it "requires api_key in params" do > >>> get "/api/v1/originator/hello", MIN_PARAMS.except(k) > >>> expect(response.status).to eq(400) > >>> expect(response.body).to include("missing parameter: #{k}") > >>> end > >>> end > >>> end > >>> > >>> WDYT? > >>> > >>> On Tue, Dec 4, 2012 at 2:17 PM, Chris Bloom <[email protected]> > >>> wrote: > >>> > I've run into another set of problems with the two solutions you > >>> > suggested. > >>> > > >>> > If I go the first way, having the macro method define the example > >>> > inside of > >>> > it, and call that from within a describes block, it appears that any > >>> > instance variables declared in the before block of the spec aren't > >>> > available. Is this correct behavior? > >>> > > >>> > Alternately, if I instead turn it into a matcher and call it from > >>> > inside an > >>> > it{} block, I'm not able to get proper result messages. > >>> > > >>> > Granted the latter problem is easily ignorable given that the test > >>> > itself > >>> > works, but I'd like to understand both problems anyway. > >>> > > >>> > Here's the code both ways, first, as a matcher: > >>> > > >>> > # spec/support/api_macros.rb > >>> > RSpec::Matchers.define :require_minimum_request_params do |url, > params| > >>> > match do |_| > >>> > get url > >>> > response.status.should == 400 > >>> > # See https://github.com/dchelimsky/rspec/issues/25 > >>> > response.body.include?("missing parameter:") > >>> > > >>> > (params.length - 1).times do |i| > >>> > params.to_a.combination(i+1).each do |c| > >>> > get url, Hash[*c.flatten] > >>> > response.status.should == 400 > >>> > response.body.include?("missing parameter:") > >>> > end > >>> > end > >>> > > >>> > get url, params > >>> > response.status.should == 200 > >>> > !response.body.include?("missing parameter:") > >>> > end > >>> > > >>> > failure_message_for_should do > >>> > "expected URL #{url} to require #{params.keys.join(', ')} as the > >>> > minimum > >>> > parameters" > >>> > end > >>> > > >>> > failure_message_for_should_not do > >>> > "expected URL #{url} to not require #{params.keys.join(', ')} as > >>> > the > >>> > minimum parameters" > >>> > end > >>> > > >>> > description do > >>> > "require minimum parameters #{params.keys.join(', ')} for > requests > >>> > to > >>> > URL #{url}" > >>> > end > >>> > end > >>> > > >>> > # spec/requests/api/api_v1.rb > >>> > describe MyApp::API_v1 do > >>> > before do > >>> > @minimum_params = { > >>> > api_key: "", > >>> > nonce: "", > >>> > timestamp: "", > >>> > hmac_digest: "" > >>> > } > >>> > end > >>> > > >>> > context "originator" do > >>> > describe "GET /api/v1/originator/hello" do > >>> > it { should > >>> > require_minimum_request_params("/api/v1/originator/hello", > >>> > @minimum_params) } > >>> > end > >>> > end > >>> > end > >>> > > >>> > # $ rspec spec/requests/api/api_v1_spec.rb > >>> > MyApp::API_v1 > >>> > originator > >>> > GET /api/v1/originator/hello > >>> > should == 200 > >>> > > >>> > And instead as a macro: > >>> > # spec/support/api_macros.rb > >>> > module ApiMacros > >>> > def self.included(base) > >>> > base.extend(ClassMethods) > >>> > end > >>> > > >>> > module ClassMethods > >>> > def it_should_require_minimum_request_params(url, params) > >>> > it "should require minimum request params" do > >>> > get url > >>> > response.status.should == 400 > >>> > response.body.should include("missing parameter") > >>> > > >>> > (params.length - 1).times do |i| > >>> > params.to_a.combination(i + 1).each do |c| > >>> > get url, Hash[*c.flatten] > >>> > response.status.should == 400 > >>> > response.body.should include("missing parameter") > >>> > end > >>> > end > >>> > > >>> > get url, params > >>> > response.status.should == 200 > >>> > response.body.should_not include("missing parameter") > >>> > end > >>> > end > >>> > end > >>> > end > >>> > > >>> > # spec/requests/api/api_v1.rb > >>> > describe MyApp::API_v1 do > >>> > before do > >>> > @minimum_params = { > >>> > api_key: "", > >>> > nonce: "", > >>> > timestamp: "", > >>> > hmac_digest: "" > >>> > } > >>> > end > >>> > > >>> > context "originator" do > >>> > describe "GET /api/v1/originator/hello" do > >>> > > >>> > it_should_require_minimum_request_params("/api/v1/originator/hello", > >>> > @minimum_params) > >>> > end > >>> > end > >>> > end > >>> > > >>> > # $ rspec spec/requests/api/api_v1_spec.rb > >>> > Failure/Error: (params.length - 1).times do |i| > >>> > NoMethodError: > >>> > undefined method `length' for nil:NilClass > >>> > # ./spec/support/api_macros.rb:13:in `block in > >>> > it_should_require_minimum_request_params > >>> > > >>> > On Monday, December 3, 2012 5:42:08 PM UTC-5, Chris Bloom wrote: > >>> >> > >>> >> Ah, OK. I see the difference now. Thanks for the clarification. > >>> >> > >>> >> On Monday, December 3, 2012 3:33:42 PM UTC-5, [email protected] > >>> >> wrote: > >>> >>> > >>> >>> On Mon, Dec 3, 2012 at 2:10 PM, Chris Bloom <[email protected]> > >>> >>> wrote: > >>> >>> > I'm trying to refactor some common code used in a bunch of > requests > >>> >>> > specs > >>> >>> > into a macro, but every way I've tried so far ends in an error > >>> >>> > saying > >>> >>> > it > >>> >>> > can't find the macro method, or if it can then it can't find the > >>> >>> > `get` > >>> >>> > method. Can someone point me to an example of how to do this? > >>> >>> > > >>> >>> > # spec/requests/api/api_v1.rb > >>> >>> > describe MyApp::API_v1 do > >>> >>> > context "originator" do > >>> >>> > describe "GET /api/v1/originator/hello" do > >>> >>> > it_should_check_minimum_protected_api_params > >>> >>> > "/api/v1/originator/hello" > >>> >>> > end > >>> >>> > end > >>> >>> > end > >>> >>> > > >>> >>> > # spec/support/api_macros.rb > >>> >>> > module ApiMacros > >>> >>> > def self.included(base) > >>> >>> > base.extend(GroupMethods) > >>> >>> > end > >>> >>> > > >>> >>> > module GroupMethods > >>> >>> > def it_should_check_minimum_protected_api_params(url) > >>> >>> > get url > >>> >>> > ... > >>> >>> > end > >>> >>> > end > >>> >>> > end > >>> >>> > > >>> >>> > # spec/spec_helper.rb > >>> >>> > RSpec.configure do |config| > >>> >>> > config.include ApiMacros, :type => :request > >>> >>> > end > >>> >>> > > >>> >>> > This ends in: > >>> >>> > > >>> >>> > $ rspec spec/requests/api > >>> >>> > /spec/support/api_macros.rb:8:in > >>> >>> > `it_should_check_minimum_protected_api_params': undefined method > >>> >>> > `get' > >>> >>> > for > >>> >>> > #<Class:0x000001036708f0> (NoMethodError) > >>> >>> > >>> >>> That's not saying it can't find the macro method. It says it can't > >>> >>> find > >>> >>> `get`. > >>> >>> > >>> >>> The macro is being evaluated at the class level, whereas "get" is > an > >>> >>> instance method. The macro needs to define examples that use the > get > >>> >>> method, e.g: > >>> >>> > >>> >>> def it_should_check_minimum_protected_api_params(url) > >>> >>> it "should check minimum protected api params" do > >>> >>> get url > >>> >>> # ... > >>> >>> end > >>> >>> end > >>> >>> > >>> >>> HTH, > >>> >>> David > >>> > > >>> > -- > >>> > You received this message because you are subscribed to the Google > >>> > Groups > >>> > "rspec" group. > >>> > To post to this group, send email to [email protected]. > >>> > To unsubscribe from this group, send email to > >>> > [email protected]. > >>> > To view this discussion on the web visit > >>> > https://groups.google.com/d/msg/rspec/-/R3BPlOkxEZIJ. > >>> > For more options, visit https://groups.google.com/groups/opt_out. > >>> > > >>> > > >>> > >>> -- > >>> You received this message because you are subscribed to the Google > Groups > >>> "rspec" group. > >>> To post to this group, send email to [email protected]. > >>> To unsubscribe from this group, send email to > >>> [email protected]. > >>> For more options, visit https://groups.google.com/groups/opt_out. > >>> > >>> > >> > > > > -- > > You received this message because you are subscribed to the Google Groups > > "rspec" group. > > To post to this group, send email to [email protected]. > > To unsubscribe from this group, send email to > > [email protected]. > > For more options, visit https://groups.google.com/groups/opt_out. > > > > > > -- > You received this message because you are subscribed to the Google Groups > "rspec" group. > To post to this group, send email to [email protected]. > To unsubscribe from this group, send email to > [email protected]. > For more options, visit https://groups.google.com/groups/opt_out. > > > -- You received this message because you are subscribed to the Google Groups "rspec" group. To post to this group, send email to [email protected]. To unsubscribe from this group, send email to [email protected]. For more options, visit https://groups.google.com/groups/opt_out.
