Christopher Bailey
2008-Sep-05 21:15 UTC
[rspec-users] Mocking a 3rd party call, but with a few exceptions
I use geocoding in our app, and it permeates most of the core functionality. Because it makes a call out to Google or Yahoo or what not to do the geocoding, I''d like to mock this for the bulk of my tests, except for the few tests that actually do stuff where they need the real data. I had started wrapping all my specs with the equivalent (but a DRY form) of: GeoInfo = Struct.new(:lat, :lng, :success) describe "with fake geocoding" do before(:all) do fake_geocode = GeoInfo.new(123.456, 789.012, true) GeoKit::Geocoders::MultiGeocoder.stub!(:geocode).and_return(fake_geocode) end # bulk of tests are here end describe ... #other tests that want real geocoding here But, that just seems like a poor way to do it. I''m wondering, how can I make GeoKit::Geocoders::MultiGeocoder fake by default, and then in the few cases where I want it to be real, "un-stub" it or whatever you''d call it? -- Christopher Bailey Cobalt Edge LLC http://cobaltedge.com -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://rubyforge.org/pipermail/rspec-users/attachments/20080905/6acf585f/attachment.html>
Pat Maddox
2008-Sep-06 03:36 UTC
[rspec-users] Mocking a 3rd party call, but with a few exceptions
On Fri, Sep 5, 2008 at 2:15 PM, Christopher Bailey <chris at cobaltedge.com> wrote:> I use geocoding in our app, and it permeates most of the core functionality. > Because it makes a call out to Google or Yahoo or what not to do the > geocoding, I''d like to mock this for the bulk of my tests, except for the > few tests that actually do stuff where they need the real data. I had > started wrapping all my specs with the equivalent (but a DRY form) of: > GeoInfo = Struct.new(:lat, :lng, :success) > describe "with fake geocoding" do > before(:all) do > fake_geocode = GeoInfo.new(123.456, 789.012, true) > > GeoKit::Geocoders::MultiGeocoder.stub!(:geocode).and_return(fake_geocode) > end > # bulk of tests are here > end > describe ... #other tests that want real geocoding here > But, that just seems like a poor way to do it. I''m wondering, how can I > make GeoKit::Geocoders::MultiGeocoder fake by default, and then in the few > cases where I want it to be real, "un-stub" it or whatever you''d call it?I would probably write a thin wrapper around that class, exposing the functionality you need with the interface you want. Then in your tests, you can 1) mock that class directly 2) use dependency injection along with rspec-built mocks 3) use dependency injection with hand-built fakes The difference between 2 and 3 is that with rspec-built mocks, you''re going to do stuff like @mock_geocoder = mock("geocoder", :geocode => fake_geocode) and with a hand-built fakes, you''d do something like class FakeGeocoder def initialize @geo_info_class = Struct.new(:lat, :lng, :success) end def geocode @geo_info_class.new(123.456, 789.012, true) end end You mentioned that this geocoding is a core feature of your app, so going with a hand-rolled fake may give you more flexibility to do some more sophisticated stuff. It has the added benefit of forcing you to really think about the abstraction in your domain since you''re going to be implementing it twice (once as a wrapper around that class, and once for testing purposes). So there are the standard ways of doing DI [1]. In cases like this, a favorite trick of mine is to have an aliased constant that points to the implementation you want. For example, in development.rb you might have Geocoder = GoogleGeocoder # GoogleGeocoder is your production wrapper and in test.rb you have Geocoder = FakeGeocoder This is kind of a clever spin (if I do say so myself :) on the Service Locator [2] pattern. Basically, instead of having one central object that knows how to map domain abstractions to implementations, you just define a constant to represent the domain abstraction, and then point it to the real implementation you want. So now your production code just references Geocoder all over the place, you write unit tests for GoogleGeocoder to make sure it works, and you get your FakeGeocoder throughout your other unit tests for free. If you need to change implementations, you can just reassign the constant and ignore the warnings...but if you plan to have multiple implementations that you use throughout the app, you''ll probably want to go for more traditional dependency injection. Pat [1] Jim Weirich gave a talk at OSCON 2005, the slides for which I can''t find anymore (!!). It basically showed traditional DI and some neat stuff you can do with Ruby to make it much simpler. [2] http://www.martinfowler.com/articles/injection.html#UsingAServiceLocator
Christopher Bailey
2008-Sep-16 18:57 UTC
[rspec-users] Mocking a 3rd party call, but with a few exceptions
I''ve played around a bit on this. The constant setting with the service locator pattern looked good, but I couldn''t get the undoing/resetting of the constant to work properly, not sure what I was doing wrong there. This appears to be a solution though: In my spec_helper.rb (which is required by all specs), I put this: config.before(:each) do # Setup fake geocoding unless told not to unless @do_not_mock_geocoding fake_geocode = OpenStruct.new(:lat => 123.456, :lng => 123.456, :success => true) GeoKit::Geocoders::MultiGeocoder.stub!(:geocode).and_return(fake_geocode) end end Then, in tests where I want real geocoding, I just set @do_not_mock_geocoding to true. On Fri, Sep 5, 2008 at 8:36 PM, Pat Maddox <pergesu at gmail.com> wrote:> On Fri, Sep 5, 2008 at 2:15 PM, Christopher Bailey <chris at cobaltedge.com> > wrote: > > I use geocoding in our app, and it permeates most of the core > functionality. > > Because it makes a call out to Google or Yahoo or what not to do the > > geocoding, I''d like to mock this for the bulk of my tests, except for the > > few tests that actually do stuff where they need the real data. I had > > started wrapping all my specs with the equivalent (but a DRY form) of: > > GeoInfo = Struct.new(:lat, :lng, :success) > > describe "with fake geocoding" do > > before(:all) do > > fake_geocode = GeoInfo.new(123.456, 789.012, true) > > > > > GeoKit::Geocoders::MultiGeocoder.stub!(:geocode).and_return(fake_geocode) > > end > > # bulk of tests are here > > end > > describe ... #other tests that want real geocoding here > > But, that just seems like a poor way to do it. I''m wondering, how can I > > make GeoKit::Geocoders::MultiGeocoder fake by default, and then in the > few > > cases where I want it to be real, "un-stub" it or whatever you''d call it? > > I would probably write a thin wrapper around that class, exposing the > functionality you need with the interface you want. Then in your > tests, you can > 1) mock that class directly > 2) use dependency injection along with rspec-built mocks > 3) use dependency injection with hand-built fakes > > The difference between 2 and 3 is that with rspec-built mocks, you''re > going to do stuff like > @mock_geocoder = mock("geocoder", :geocode => fake_geocode) > > and with a hand-built fakes, you''d do something like > > class FakeGeocoder > def initialize > @geo_info_class = Struct.new(:lat, :lng, :success) > end > > def geocode > @geo_info_class.new(123.456, 789.012, true) > end > end > > You mentioned that this geocoding is a core feature of your app, so > going with a hand-rolled fake may give you more flexibility to do some > more sophisticated stuff. It has the added benefit of forcing you to > really think about the abstraction in your domain since you''re going > to be implementing it twice (once as a wrapper around that class, and > once for testing purposes). > > So there are the standard ways of doing DI [1]. In cases like this, a > favorite trick of mine is to have an aliased constant that points to > the implementation you want. For example, in development.rb you might > have > Geocoder = GoogleGeocoder # GoogleGeocoder is your production wrapper > > and in test.rb you have > Geocoder = FakeGeocoder > > This is kind of a clever spin (if I do say so myself :) on the Service > Locator [2] pattern. Basically, instead of having one central object > that knows how to map domain abstractions to implementations, you just > define a constant to represent the domain abstraction, and then point > it to the real implementation you want. So now your production code > just references Geocoder all over the place, you write unit tests for > GoogleGeocoder to make sure it works, and you get your FakeGeocoder > throughout your other unit tests for free. > > If you need to change implementations, you can just reassign the > constant and ignore the warnings...but if you plan to have multiple > implementations that you use throughout the app, you''ll probably want > to go for more traditional dependency injection. > > Pat > > > [1] Jim Weirich gave a talk at OSCON 2005, the slides for which I > can''t find anymore (!!). It basically showed traditional DI and some > neat stuff you can do with Ruby to make it much simpler. > > [2] > http://www.martinfowler.com/articles/injection.html#UsingAServiceLocator > _______________________________________________ > rspec-users mailing list > rspec-users at rubyforge.org > http://rubyforge.org/mailman/listinfo/rspec-users >-- Christopher Bailey Cobalt Edge LLC http://cobaltedge.com -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://rubyforge.org/pipermail/rspec-users/attachments/20080916/1a479c31/attachment-0001.html>