I'm looking for a reliable way to dynamically stub certain methods in my development environment. One example use case is when I need to do development that normally requires access to the Facebook Graph APIs but I don't have Internet access. I'd like to be able to stub the calls to fb_graph methods so it looks as if I'm authenticated and have profile data. Ideally I could turn the stubs on or off with a minor config change.
Any ideas? I'm assuming something like mocha can handle this?
You can use the VCR gem which will record the results of an initial HTTP request into a yml file and then use the contents of that yml file on subsequent http requests. It can then be configured to ignore the VCR logic and always make HTTP requests, if so desired:
https://www.relishapp.com/myronmarston/vcr
Mocha can certainly do it. But it feels a bit strange.
You could also do something like dependency injection.
For instance:
class User < AR::Base
def find_friends
Facebook.find_friends(facebook_id)
end
end
class Facebook
def self.find_friends(id)
# connect to Facebook here
end
end
class FakeFacebook
def self.find_friends(id)
# a fake implementation here
end
end
And inside an initializer:
if Rails.env.development?
User::Facebook = FakeFacebook
end
Related
I am building an API with Rails, using the rails-api gem. I want to use cucumber-rails and the gem 'Airborne' to test it.
Airborne comes with some nice helper methods for testing API responses, which I want to have access to in my step definitions. I have done this kind of thing before in Sinatra, which was relatively straightforward to configure in the /features/env.rb file.
It seems, however, that with rails-cucumber the creation of the 'World' happens behind the scenes somewhere and I don't know how to extend it to use the Airborne module after it's been created.
I have tried the following:
Airborne.configure do |config|
config.rack_app = Rails.application
end
Cucumber::Rails::World.extend(Airborne)
When(/^I make a request for information about an event$/) do
get "/events/1"
end
Then(/^I receive the information as a JSON$/) do
expect_json {}
end
I am still getting a NoMethodError on #expect_json, which is an Airborne method.
So my question is: how can I extend the instance of World in the context of cucumber-rails?
Don't panic, the World has been saved. The solution is to wrap Airborne and whatever else in a module:
module MyHelpers
include Airborne
include Capybara::DSL
end
Then pass that:
World(MyHelpers)
I wanted to use this api: https://github.com/coinbase/coinbase-ruby and the first step is to initialize the API, like this:
coinbase = Coinbase::Client.new(ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET'])
I was wondering what the best place to put this code is, and how would I access it if I put it "there"? I want this variable (coinbase) to be accessible ANYWHERE in the application.
Thanks!
The answer to this question really depends on your use case and your approach. My geral recommendation, however, is to create a Service Object (in the DDD sense) (see the section named "Domain Objects Should Not Know Anything About Infrastructure Underneath" in that link), that handles all communication with the Coinbase API. And then, within this service object, you can simply initialize the Coinbase::Client object once for however many times you call into it. Here's an example:
# app/services/coinbase_service.rb
class CoinbaseService
cattr_reader :coinbase_client, instance_accessor: false do
Coinbase::Client.new(ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET'])
end
def self.do_something
coinbase_client.do_something_in_their_api
end
def self.do_something_else
coinbase_client.do_something_else_in_their_api
end
end
So then you might do, e.g.:
# From MyController#action_1
if CoinbaseService.do_something
# ...
else
# ...
end
Or:
# From MyModel
def do_something
CoinbaseService.do_something_else
end
To get the service object working, you may need to add app/services to your autoload paths in application.rb file. I normally just add this:
# config/application.rb
config.autoload_paths += %W(#{config.root}/app)
I find this Service Object approach to be very beneficial organizationally, more efficient (only 1 invocation of the new Coinbase client needed), easier to test (easy to mock-out calls to Coinbase::Client), and simply joyful :).
One way to go about having a global variable can be done as similar as initializing redis in a Rails application by creating an initializer in config/initializers/coinbase.rb with:
$coinbase = Coinbase::Client.new(ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET'])
Now, you can access $coinbase anywhere in the application!
In the file config/initializers/coinbase.rb
Rails.application.config.after_initialize do
CoinbaseClient = Coinbase::Client.new(
Rails.application.credentials.coinbase[:api_key],
Rails.application.credentials.coinbase[:api_secret])
end
In place of the encrypted credentials, you could also use environment variables: ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET']
The above will make the constant CoinbaseClient available everywhere in your app. It will also ensure all your gems are loaded before the client is initialized.
Note: I am using Rails 6.1.4.4, and Ruby 2.7.5
I'm doing a code that need to search a external API, but during development I haven't access to this API, so my current solution to run the server and navigate through the system is:
def api_call
return { fake: 'This is a fake return' } if Rails.env.development?
# api interaction code
# ...
end
This let my code dirt, so my question is: There are a pattern (or a better way) to do this?
The pattern I use is to replace api object with one that fakes all methods when in development.
class Api
def query
# perform api query
end
end
class FakeApi
def query
{ fake: 'This is a fake return' }
end
end
# config/environments/production.rb
config.api = Api.new
# config/environments/test.rb
# config/environments/development.rb
config.api = FakeApi.new
# then
def api_call
Rails.configuration.api.query # no branching here! code is clean
end
Basically, you have two classes, Api which does real work and FakeApi that returns pre-baked faked responses. You then use Rails' environment configuration to set different apis in different environments. This way, your client code (that calls #query) doesn't have to care about current environment.
Webmock (https://github.com/bblimke/webmock) is generally accepted as the best gem to stub out external services, and has the added benefit of letting you test how your api_call method parses the API's response.
I have a simple function to prevent emails from being sent to customers when testing locally:
def safe_emails emails
if Rails.env == 'production'
emails
else
emails.select{|e| ['staff#example.com', 'staff2#example.com'].include?(e) }
end
end
I want to share that function between mailers. I can see two options, a module or a class method.
Option 1: Module
class ReportMailer < ActionMailer::Base
include SafeEmailer
def daily emails
mail_to: safe_emails(emails)
end
end
Option2: Class Method
class ReportMailer < ActionMailer::Base
def daily emails
mail_to: SafeEmailer.safe_emails(emails)
end
end
The class method is a no-no according to some due to global scope, including a module with one method doesnt seem all that attractive. Monkey-patching ActionMailer to throw the method in there also seems like it could cause trouble (when Rails 4.3 introduces the safe_emails method or whatever).
I would go with module option even if it's a simple one function module. Keeping a generic function that can eventually be used by multiple classes in a module makes much more sense than defining it inside a class.
If Rails 4.3 is of your concern then you can simply replace your include MySafeEmailModule with whatever Rails 4.3 would include this function in, as compared to find and replace all calls to ReportMailer.daily_emails.
Neither – in your case, you'd need a policy object, that decides who receives email regarding the Rails.env. I'd keep that logic outside the ReportMailer.
I'd go with something like:
UserMailer.welcome_email(#user).deliver if SafeEmailer.new(#user.email).safe?
This is probably the easiest way.
Set the below configuration in your non production environments.(config/environments/.rb)
config.action_mailer.delivery_method = :smtp (default), :sendmail, :test, or :file
Have a look at letter opener gem. You could have the delivery_method set as letter_opener and have the emails open in browser instead of actually sending them in your non production environments.
In my Rails 3 application I have a controller with the following actions (code simplified):
def payment
redirect_to some_url_of_an_external_website
end
# the external website redirects the browser to this action when the payment is done
def payment_callback
#subscription = Subscription.new(:subscription_id => params[:subscription_id])
...
end
In my acceptance test (using steak and RSpec 2), I want to avoid the redirection to this external URL when capybara follows the link pointing to the payment action. Basically I want to mock the route helper payment_path so that it directly points to the payment_call_path with the appropriate subscription_id parameter.
Is this the correct way to do it? If so, how can I mock the payment_path (could not find how to do it)?
Whilst I usually try to avoid mocking in integration tests, here you can do something like this:
MyController.stub!(:payment).and_return('payment received').
Using class_eval as mentioned above will lead to that method being permanently stubbed out across your entire run (if you want this, I'd suggest stubbing it in spec_helper [that's assuming you use a spec_helper]). I find using rspec's mocking/stubbing stuff preferable anyway.
I'm not sure if this is the 'correct' way of doing this, but you can stub any of your application's code to return what you need for your test. So somewhere in your RSpec test you can do something like
MyController.class_eval do
def payment
'payment received'
end
end
Here is an example (see section 'Session Helper Methods') where the #admin? method in ApplicationController is stubbed when a custom RSpec helper module is included into the example group.