Stub method called from constructor when using Fabrication

1.1k views Asked by At

I've got the following model

class User < ActiveRecord::Base
  before_create :set_some_values

  private
  def set_some_values
    #do something
  end
end

In specs I'm using Fabrication gem to create objects but I can't find a way to stub the set_some_values method. I tried

User.any_instance.stub!(:set_some_values).and_return(nil)

but Fabrication seems to ignore this. Is it possible to do?

2

There are 2 answers

1
Elliot Winkler On BEST ANSWER

This is why I don't like ActiveRecord callbacks -- because if you want to have nothing to do with a callback (because, say, you're making a call to an external service inside the callback) you still have to be concerned about stubbing it out. Yes you could stub out methods inside the callback, but it's the same problem, and actually it's a bit worse because now you are concerned about something inside a method you want nothing to do with.

As usual there are multiple options here.

One option which I've used a lot in the past is, add a condition to your callback that turns it off by default. So your Post class could look like:

class Post
  before_save :sync_with_store, :if => :syncing_with_store?

  def syncing_with_store?; @syncing_with_store; end
  attr_writer :syncing_with_store

  def sync_with_store
     # make an HTTP request or something
  end
end

Now wherever you really want to call the callback (perhaps in your controller or wherever), you can set post.syncing_with_store = true before you call post.save.

The downside to this approach is, it's something that you (and other devs working with you) have to keep in mind, and it's not really obvious that you have to do this. On the other hand, if you forget to do this, nothing bad happens.

Another option is to use a fake class. Say you have a Post that pushes its data to an external data store on save. You could extract the code that does the pushing to a separate class (e.g. Pusher) which would be accessible at Post.pusher_service. By default, though, this would be set to a fake Pusher class that responds to the same interface but does nothing. So like:

class Post
  class << self
    attr_accessor :pusher_service
  end
  self.pusher_service = FakePostPusher

  before_save :sync_with_store

  def sync_with_store
    self.class.pusher_service.run(self)
  end
end

class FakePostPusher
  def self.run(post)
    new(post).run
  end

  def initialize(post)
    @post = post
  end

  def run
    # do nothing
  end
end

class PostPusher < FakePostPusher
  def run
    # actually make the HTTP request or whatever
  end
end

In your production environment file, you'd set Post.pusher_service = Pusher. In individual tests or test cases, you'd make a subclass of Post -- let(:klass) { Class.new(Post) } -- and set klass.pusher_service = Pusher (that way you don't permanently set it and affect future tests).

The third approach, which I have been experimenting with, is this: simply don't use ActiveRecord callbacks. This is something I picked up from Gary Bernhardt's screencasts (which, by the way, are pretty amazing). Instead, define a service class that wraps the act of creating a post. Something like:

class PostCreator
  def self.run(attrs={})
    new(attrs).run
  end

  def initialize(attrs={})
    @post = Post.new(attrs)
  end

  def run
    if @post.save
      make_http_request
      return true
    else
      return false
    end
  end

  def make_http_request
    # ...
  end
end

This way PostCreator.run(attrs) is the de facto way of creating a post instead of going through Post. Now to test saves within Post, there's no need to stub out callbacks. If you want to test the PostCreator process, there's no magic going on, you can easily stub out whichever methods you want or test them independently. (You could argue that stubbing out methods here is the same as stubbing out AR callbacks, but I think it's more explicit what's going on.) Obviously this only handles post creation, but you could do the same for post updating too.

Anyway, different ideas, pick your poison.

3
Elliot Winkler On

The #set_some_values method here is called when you call #save on the record. So it has nothing to do with the constructor and therefore you don't need to stub User.any_instance -- just make your record and then do a partial stub, as in:

record.stub(:set_some_values)
record.save