Experimenting with explicit contracts with Ruby

A few months back, José Valim started a conversation on overusing mocks and coupling between components. That made me interested on revisiting how I design my code and it has changed my approach to testing a bit in one of our current Ruby projects.

A Tale of Two Adapters

Back in November, I worked on integrating a payment gateway from scratch into one of our client projects, through a gem that abstracts the HTTP interface of this external service. On this payment flow we had to first authorize the payment data with the gateway, which would return the transaction data for us to capture the payment in the background and go on with the business logic that depended on a successful payment flow.

If you ever worked on something similar, you probably remember a few rough edges that we need to deal in cases like this: how to test the integration with the right credit card numbers for each possible outcome? Do we have a sandbox available for development and test environments? How can we control the performance and stability costs that this external dependency might bring to our application, and the coupling between the code that supports this core feature and this gem?

Our attempt to handle the coupling and maintenance cost of this dependency was to push all the integration code behind an abstraction layer responsible for dealing with this payment flow logic under a Payments namespace.

require 'gateway-xyz-gem'

module Payments
  class GatewayXYZ
    def authorize(order, credit_card)
      # Uses the `Order` details and the user `CreditCard` data to authorize
      # a new transaction on the XYZ Payment Gateway through the
      # `gateway-xyz-gem` classes.
    end

    def capture(payment_id)
      # Capture the payment information for a transaction that was previously
      # authorized.
    end
  end
end

Somewhere down our orders#create action (but not directly in the controller method itself) we call GatewayXYZ#authorize with the order record and a credit_card value object and our integration with the external service is done.

We might have a nice set of well-defined methods on the GatewayXYZ class but our job on these abstractions is far from done. We might unit test it with something like WebMock or VCR to handle the external service dependency, but every other piece of our system that interacts with this abstraction will also depend on the external API to work properly – the OrdersController, the background job that captures the payment and the Order model itself that might trigger the initial authorize call. Should we just sprinkle the existing stubs all over our test suite and call it a day?

We added a gateway implementation that mimics the expected behavior of the GatewayXYZ (with the same method signatures as the real gateway) and doesn’t depend on external resources. It also has a predefined behavior for specific inputs so we can test different code paths of their collaborators based on the test input.

module Payments
  class Memory
    def authorize(order, credit_card)
      if BAD_CREDIT_CARD_NUMBERS.include?(credit_card.number)
        bad_response
      else
        ok_response
      end
    end
  end
end

Dealing with environment specific setups

Now we need to make our Payments::Memory the go-to implementation for our test cases that depend on our payment abstractions. There are a few different ways we can do this on a Rails app.

Rails.application.config

We can expose a configuration setting in app that says which implementation it should use, similar to how Action Mailer picks the delivery method for your emails or how Active Job might have different queue adapters for your background jobs.

# config/application.rb
Rails.application.config.x.payment_gateway = Payments::GatewayXYZ

# config/environments/test.rb
Rails.application.config.x.payment_gateway = Payments::Memory

# app/models/order.rb
class Order
  def authorize(credit_card)
    gateway = build_gateway
    gateway.authorize(self, credit_card)
  end

  private

  def build_gateway
    klass = Rails.application.config.x.payment_gateway
    klass.new
  end
end

Module.mattr_accessor macro

You can set a class level macro on the classes that depend on a configurable value and change as you want in your code. This approach can be useful if you want to keep the configuration closer to the implementation that relies on it, instead of jumping between app code and configuration code if you want to debug something or be able to change it during runtime.

# app/models/order.rb
class Order
  cattr_accessor :payment_gateway do
    Payments::GatewayXYZ
  end

  def authorize(credit_card)
    gateway = payment_gateway.new
    gateway.authorize(self, credit_card)
  end
end

# test/test_helper.rb
Order.payment_gateway = Payments::Memory

Factory method

This approach is useful when you want to hide away how to create an instance of a gateway implementation, so other classes that depend on it can have a way to just ask for a gateway object without worrying on how to create it.

# app/models/payments.rb
module Payments
  matt_accessor :gateway do
    Payments::GatewayXYZ
  end

  def build_gateway
    gateway.new
  end

  module_function :build_gateway
end

# test/test_helper.rb
Payments.gateway = Payments::Memory

I don’t believe that there is a Single Way to Do It™ this kind of dependency injection, so you should feel free to pick a strategy that suits the interfaces you are building and the coding style of your team – I’m personally a fan of the factory method and the cattr_accessor approaches as they feel more detached from the configuration and closer to the application code, although the configuration way feels more aligned with global APIs from 3rd party gems.

Skipping Hash driven development

Our GatewayXYZ and Memory implementations have the same methods with the same arguments but there is a second piece of making a uniform API that we need to think about: what those methods should return?

Our authorize needs to return more than a truthy/falsy value, as we need to gather more information about the payment transaction on our end, like the payment_id from the transaction, or a reason of why it might have failed (was the credit card denied? There is invalid data in the request), details for logging or instrumentation, etc. And if we think about implementing this API for multiple services (let’s say we need a Payments::PayPal now, for instance), those services will return this data in different formats that we need to normalize so these differences don’t leak to the rest of the system.

One might say that a Hash with all this junk would do it, but going that path opens too many doors for inconsistency and bugs as the hash is a weak abstraction that can be mutated anywhere and won’t enforce any specific format or requirements on the return values.

For that, we can implement a Payments::Result struct/value object to represent the outcome of our authorize action, and return it from each gateway implementation in our system, enforcing the interface we want to have.

module Payments
  class Result < Struct.new(:payment_id, :errors)
    def ok?
      errors.blank?
    end

    def failed?
      !ok?
    end
  end
end

Our Result class has the minimal information that our client code needs, and each gateway is responsible for constructing a Result from its own data. The Memory gateway can do something as straightforward as this:

module Payments
  class Memory
    def authorize(order, credit_card)
      Result.new(
        payment_id: SecureRandom.hex,
        errors: SAMPLE_ERRORS[credit_card.number])
    end
  end
end

This approach is useful not just for enforcing the interface we want, but also to improve other areas of our code that could use more specific abstractions than a bare Hash instance.

Going forward with contracts and macros

This homemade approach for better contracts between our app and this external service can go a long way, but if you want, you can build strict checks on top of your APIs to ensure that your objects are collaborating as you expect. We haven’t tried yet, but the contracts gem looks very interesting if you want that kind of type constraints that are lacking on Ruby.

You can even write your own checks by wrapping methods into type checking proxies, as refile does with its Refile::BackendMacros module. When extended by a backend implementation, it provides macros to validate the input for methods like #upload(uploadable) or #delete(id), so custom implementations don’t need to worry about validating these arguments on their own.

Subscribe to our blog
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someone
  • Great article, Lucas! Thanks for sharing.

    We have a similar gateway integration, and at the beginning of the implementation we ignored the cost of using the “real” implementation in our tests. We had to use mocks/stubs or VCR in a lot of places.

    The memory adapter would avoid a lot of pain (with VCR) and repetition (mocks/stubs) in our code.

  • suryart

    Nice article.
    There’s a type though. I think it should be `module Payments` instead of `module Payment` under “Skipping Hash driven development” where you’re creating `Payments::Result` class.

  • Lucas Mazza

    Fixed, thanks!