Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom helper method with hooks #1378

Closed
batter opened this issue Mar 10, 2014 · 4 comments
Closed

Custom helper method with hooks #1378

batter opened this issue Mar 10, 2014 · 4 comments

Comments

@batter
Copy link

batter commented Mar 10, 2014

This is not an issue, but a question that I've been trying to wrap my head around and have not found a good answer to yet. If this is documented or this is not the proper forum for this type of thing then please point me in the right direction as to where I should go.

Please see paper-trail-gem/paper_trail#312 for some background on what I'm trying to accomplish. The idea here is that I want to add a helper method which has the ability to hook into the run-sequence for the test contents of the block within it.

We have an RSpec helper that adds a before(:each) hook to the RSpec configuration which sets the default value for a method that is essentially an attr_accessor field. You can view this code here. There is a custom metadata, :versioning => true that can be attached, which hooks into the before sequence, and reverses the value of that boolean. There is also a custom helper method, with_versioning, that I include, (and in retrospect should probably be extending as well) into the RSpec configuration. Here is the code for the custom helper method. Basically, I want to be able to do something like this with my RSpec syntax (pretend the with_versioning method is extended as a class method to RSpec's configuration):

describe PaperTrail do
  it "due to the `before` configuration hook, is disabled by default within a spec" do
    PaperTrail.should_not be_enabled
  end

  it "gets enabled when it has the `:versioning` metadata passed in", versioning: true do
    PaperTrail.should be_enabled
  end

  describe "in a describe group with `:versioning` metadata, it still works", versioning: true do
    it { PaperTrail.should be_enabled }
  end

  it "`with_versioning` works within an `it` block when `included`" do
    PaperTrail.should_not be_enabled
    with_versioning do
      PaperTrail.should be_enabled
    end
    PaperTrail.should_not be_enabled
  end

  # And I am hoping that the custom hook method could accomplish the same thing
  # but it doesn't work, and I've come to the realization that this is because it's not
  # hooking into the run sequence, and instead gets evaluated at Load Time.
  with_versioning do
    # this test fails!
    it "gets enabled within a `with_versioning` block" do
      PaperTrail.should be_enabled
    end
  end
end

I've tried switching the syntax of the with_versioning helper method in the hopes that I might be able to get it to insert before(:each) hooks on the fly (or describe '', versioning: true), but that hasn't worked. Is there a way to get what I'm trying to accomplish here to work? Any help / suggestions would be much appreciated.

@myronmarston
Copy link
Member

I think this'll do what you want:

module PaperTrail
  module RSpec
    module ExampleMethods
      # :call-seq:
      # with_versioning
      #
      # enable versioning for specific blocks

      def with_versioning
        was_enabled = ::PaperTrail.enabled?
        ::PaperTrail.enabled = true
        begin
          yield
        ensure
          ::PaperTrail.enabled = was_enabled
        end
      end
    end

    module Macros
      def with_versioning(&block)
        context "with versioning" do
          around(:each) { |ex| with_versioning(&ex) }
          class_exec(&block)
        end
      end
    end
  end
end

RSpec.configure do |rspec|
  rspec.include PaperTrail::RSpec::ExampleMethods
  rspec.extend  PaperTrail::RSpec::Macros
end
  • To make the method available to be called from within a describe or context block you need to extend the module, so we make a separate macros module for that case.
  • While not strictly necessary, I recommend having with_versioning create a nested context, because if users do this:
describe "whatever" do
  it "does x" do
  end

  with_versioning do
    before(:each) { do_something }
    it "does y" do
    end
  end
end

...then I think it would be confusing for the before hook within with_versioning to be applied to external examples, but without wrapping it in a nested group it would do that.

  • You can use an around hook to leverage the ExampleMethods definition of with_versioning to automatically wrap examples with that.
  • class_exec is needed so that the block is eval'd with the same method definition scope as it normally has inside a context block.

@JonRowe
Copy link
Member

JonRowe commented Mar 11, 2014

The macro version could also just create a context with the metadata automatically set, rather than adding an additional hook.

@batter
Copy link
Author

batter commented Mar 11, 2014

@myronmarston - Cheers! I was able to get it to work by using most of your suggestions and doing something very similar. The key was the class_exec. I hadn't used that method within Ruby before now (only class_eval). Guess I should've looked a little more closely through the core library.

I wasn't able to get the around syntax to work, but I was able to find an alternative solution.

Here's what I ended up doing for the implementation if you're curious. You are correct, it was necessary to wrap the contents of the block received inside of a context block in order to hook into the before hook sequence properly. Thanks again! 😄

@batter batter closed this as completed Mar 11, 2014
@myronmarston
Copy link
Member

The key was the class_exec. I hadn't used that method within Ruby before now (only class_eval).

class_eval would work there as well. They differ only in a few respects: whether string eval is supported and yielded arguments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants