{"id":5132,"date":"2016-02-29T10:25:59","date_gmt":"2016-02-29T13:25:59","guid":{"rendered":"http:\/\/blog.plataformatec.com.br\/?p=5132"},"modified":"2016-03-09T11:11:24","modified_gmt":"2016-03-09T14:11:24","slug":"experimenting-with-explicit-contracts-with-ruby","status":"publish","type":"post","link":"http:\/\/blog.plataformatec.com.br\/2016\/02\/experimenting-with-explicit-contracts-with-ruby\/","title":{"rendered":"Experimenting with explicit contracts with Ruby"},"content":{"rendered":"
A few months back, Jos\u00e9 Valim started a conversation<\/a> 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.<\/p>\n 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.<\/p>\n 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?<\/p>\n 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 Somewhere down our We might have a nice set of well-defined methods on the We added a gateway implementation that mimics the expected behavior of the Now we need to make our We can expose a configuration setting in app that says which implementation it should use, similar to how 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.<\/p>\n 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.<\/p>\n I don\u2019t believe that there is a Single Way to Do It\u2122 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\u2019m personally a fan of the factory method and the Our Our One might say that a For that, we can implement a Our 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 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<\/a> gem looks very interesting if you want that kind of type constraints that are lacking on Ruby.<\/p>\n You can even write your own checks by wrapping methods into type checking proxies, as A few months back, Jos\u00e9 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 … \u00bb<\/a><\/p>\n","protected":false},"author":17,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0,"footnotes":""},"categories":[1],"tags":[60,96],"aioseo_notices":[],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/5132"}],"collection":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/users\/17"}],"replies":[{"embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/comments?post=5132"}],"version-history":[{"count":9,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/5132\/revisions"}],"predecessor-version":[{"id":5178,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/5132\/revisions\/5178"}],"wp:attachment":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/media?parent=5132"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/categories?post=5132"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/tags?post=5132"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}A Tale of Two Adapters<\/h2>\n
Payments<\/code> namespace.<\/p>\n
require 'gateway-xyz-gem'\n\nmodule Payments\n class GatewayXYZ\n def authorize(order, credit_card)\n # Uses the `Order` details and the user `CreditCard` data to authorize\n # a new transaction on the XYZ Payment Gateway through the\n # `gateway-xyz-gem` classes.\n end\n\n def capture(payment_id)\n # Capture the payment information for a transaction that was previously\n # authorized.\n end\n end\nend\n<\/code><\/pre>\n
orders#create<\/code> action (but not directly in the controller method itself) we call
GatewayXYZ#authorize<\/code> with the
order<\/code> record and a
credit_card<\/code> value object and our integration with the external service is done.<\/p>\n
GatewayXYZ<\/code> class but our job on these abstractions is far from done. We might unit test it with something like WebMock<\/a> or VCR<\/a> 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<\/code>, the background job that captures the payment and the
Order<\/code> model itself that might trigger the initial
authorize<\/code> call. Should we just sprinkle the existing stubs all over our test suite and call it a day?<\/p>\n
GatewayXYZ<\/code> (with the same method signatures as the real gateway) and doesn\u2019t 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.<\/p>\n
module Payments\n class Memory\n def authorize(order, credit_card)\n if BAD_CREDIT_CARD_NUMBERS.include?(credit_card.number)\n bad_response\n else\n ok_response\n end\n end\n end\nend\n<\/code><\/pre>\n
Dealing with environment specific setups<\/h2>\n
Payments::Memory<\/code> 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.<\/p>\n
Rails.application.config<\/code><\/h3>\n
Action Mailer<\/code> picks the delivery method for your emails or how
Active Job<\/code> might have different queue adapters for your background jobs.<\/p>\n
# config\/application.rb\nRails.application.config.x.payment_gateway = Payments::GatewayXYZ\n\n# config\/environments\/test.rb\nRails.application.config.x.payment_gateway = Payments::Memory\n\n# app\/models\/order.rb\nclass Order\n def authorize(credit_card)\n gateway = build_gateway\n gateway.authorize(self, credit_card)\n end\n\n private\n\n def build_gateway\n klass = Rails.application.config.x.payment_gateway\n klass.new\n end\nend\n<\/code><\/pre>\n
Module.mattr_accessor<\/code> macro<\/h3>\n
# app\/models\/order.rb\nclass Order\n cattr_accessor :payment_gateway do\n Payments::GatewayXYZ\n end\n\n def authorize(credit_card)\n gateway = payment_gateway.new\n gateway.authorize(self, credit_card)\n end\nend\n\n# test\/test_helper.rb\nOrder.payment_gateway = Payments::Memory\n<\/code><\/pre>\n
Factory method<\/h2>\n
# app\/models\/payments.rb\nmodule Payments\n matt_accessor :gateway do\n Payments::GatewayXYZ\n end\n\n def build_gateway\n gateway.new\n end\n\n module_function :build_gateway\nend\n\n# test\/test_helper.rb\nPayments.gateway = Payments::Memory\n<\/code><\/pre>\n
cattr_accessor<\/code> 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.<\/p>\n
Skipping Hash driven development<\/h2>\n
GatewayXYZ<\/code> and
Memory<\/code> 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?<\/p>\n
authorize<\/code> needs to return more than a
truthy<\/code>\/
falsy<\/code> value, as we need to gather more information about the payment transaction on our end, like the
payment_id<\/code> 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<\/code> 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.<\/p>\n
Hash<\/code> 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.<\/p>\n
Payments::Result<\/code> struct\/value object to represent the outcome of our
authorize<\/code> action, and return it from each gateway implementation in our system, enforcing the interface we want to have.<\/p>\n
module Payments\n class Result < Struct.new(:payment_id, :errors)\n def ok?\n errors.blank?\n end\n\n def failed?\n !ok?\n end\n end\nend\n<\/code><\/pre>\n
Result<\/code> class has the minimal information that our client code needs, and each gateway is responsible for constructing a
Result<\/code> from its own data. The
Memory<\/code> gateway can do something as straightforward as this:<\/p>\n
module Payments\n class Memory\n def authorize(order, credit_card)\n Result.new(\n payment_id: SecureRandom.hex,\n errors: SAMPLE_ERRORS[credit_card.number])\n end\n end\nend\n<\/code><\/pre>\n
Hash<\/code> instance.<\/p>\n
Going forward with contracts and macros<\/h2>\n
refile<\/code><\/a> does with its
Refile::BackendMacros<\/code><\/a> module. When extended by a backend implementation<\/a>, it provides macros to validate the input for methods like
#upload(uploadable)<\/code> or
#delete(id)<\/code>, so custom implementations don’t need to worry about validating these arguments on their own.<\/p>\n