Testing Rails controllers communicating with external web services

in
As I was writing the code to accept payments through Paypal for MyOwnDB, I kept wondering how I could write functional tests to validate it. All those methods initiate or react to communications with Paypal, and although there's a sandbox available, you cannot predict the id assigned to the transactions and that must be returned to Paypal (see below for how this validation works)., This clearly limited the scope of the tests I could write. So, when Tobias Lütke, the author of the Ruby Paypal library, told me about the mock_methods he had introduced in the 1.9 release of the library, I got really excited. It enables you to redefine methods only for the code executed in the block passed. And this is exactly what I needed! Let's see how this can be used.... When Paypal initiate a communication with your site, you have to post a request back to validate the initial request and prevent invalid transactions for example by frauder. This is done with the Net::HTTP#post method, and it is that method that needs to be overwritten for the duration of the tests. But first, using the Paypal sandbox, and the breakpoint library, we need to collect sample data sent by paypal to our controller (both when Paypal initiates the communication, and when it reacts to our request for validation). First, Paypal initiated communication ; For IPN communications, you need to capture the request.raw_post value, and not the params value. For PDT communications, all parameters are passed as GET variables, and collecting the params value is sufficient. Second, the response we get from our validation post to Paypal; To get sample data, put a breakpoint in the acknowledge method (in the lib/notification.rb for IPN, in lib/payment_data.rb for PDT) where you have access to Paypal response' body. IPN communications When you receive an IPN, the body of the response to the validating post will either be "VERIFIED" or "INVALID". The code in the paypal library is like this
#build the http object
http = Net::HTTP.new(uri.host, uri.port)
#build the post request
request = Net::HTTP::Post.new(request_path)
#send the request
request = http.request(request, payload)
#check that we get the expected response
request.body == "VERIFIED"
We see that we'll need to overwrite the Net::HTTP#request method to return an object whose #body method either returns "VERIFIED" or "INVALID". And that's surprisingly easy. First, we define the mock_methods method (copy/paste Tobias' code from the library) in the test file for the controller handling IPN and PDT requests,
class Module
  def mock_methods(mock_methods)
    raise "mock methods needs a block" unless block_given?

    original    = self
    namespace   = original.name.split("::")
    class_name  = namespace.last

    mod = namespace[0..-2].inject(Object) { |mod, part| mod.const_get(part) }

    klass = (original.is_a?(Class) ? Class : Module).new(self) do

      instance_eval do
        mock_methods.each do |method, proc|
          define_method("mocked_#{method}", &proc)
          alias_method method, "mocked_#{method}"
        end
      end

    end

    begin
      mod.send(:remove_const, class_name)
      mod.const_set(class_name, klass)

      yield
    ensure
      mod.send(:remove_const, class_name)
      mod.const_set(class_name, original)
    end

  end
end
and define the class with the #body instance method returning "VERIFIED":
$paypal_verified = Class.new do
  def body; "VERIFIED"; end
end
As noted above, we need to work on the raw_post of the request, and we need to pass the sample data collected above in the same way in our tests:
   @request.env["RAW_POST_DATA"]="residence_country=BE&payment_gross=&business=rb%40example.com&\\
payer_email=raphinou%40example.com&receiver_id=BR8XMD2RJUDHJ&payment_status=Completed&\\
receiver_email=rb%40example.com&subscr_id=S-6C766433G88379323&\\
verify_sign=AsZRJg6AEx2SrK5YxjB-31tBqcbZAgaSGGCw3iAyKgGXLjKuj294KX4J&\\
action=ipn&txn_type=subscr_payment&mc_currency=EUR&item_name=Store+purchase&\\
txn_id=0BX53386WT106592J&charset=windows-1252&controller=payments&payer_status=verified&\\
payment_fee=¬ify_version=2.1&payment_date=07%3A08%3A21+Apr+29%2C+2006+PDT&mc_fee=0.86&\\
payment_type=instant&test_ipn=1&first_name=raph&last_name=bauduin&payer_id=QBBLVQQ9Y7AKY&mc_gross=9.99&item_number=908"
And here's the best part. We can now fake communication with Paypal for validating the IPN communication:
    Net::HTTP.mock_methods( :request => Proc.new { |r, b| $paypal_verified.new } ) do
      post :ipn
    end
As all activity acting on the call to ipn (which is the method corresponding to the IPN URL I configured in Paypal) happens in the block for which the request method is redefined to return a $paypal_verified instance, this call
request = http.request(request, payload)
has the same effect as
request = $paypal_verified.new
PDT communications PDT handling code can be tested the same way. Response to validating request are more complex though, but you can define it as simply as for IPN tests. Here's an example:
$paypal_success_pdt_accont_3 = Class.new do
  def body; "SUCCESS\npayment_date=07%3A35%3A49+May+01%2C+2006+PDT\ntxn_type=subscr_payment\nsubscr_id=S-1GM906155Y728363V\n\\
last_name=bauduin\nresidence_country=BE\nitem_name=Store+purchase\npayment_gross=\nmc_currency=EUR\nbusiness=rb%40example.com\n\\
payment_type=instant\npayer_status=verified\npayer_email=raphinou%40example.com\ntxn_id=2BN13399JN772484B\n\\
receiver_email=rb%40example.com\nfirst_name=raph\npayer_id=QBBLVQQ9Y7AKY\nreceiver_id=BR8XMD2RJUDHJ\nitem_number=3\n\\
payment_status=Completed\npayment_fee=\nmc_fee=0.86\nmc_gross=9.99\ncharset=windows-1252\n"; end
end