Ruby and the lack of IoC Containers
Inversion of Control, is a pattern that allows us to unit test our PHP code easier. It enables easier Dependency Injection; for those who aren’t familiar with this concept we’ll go over it below.
This section will cover a few advanced concepts in one, only because they are so interconnected.
If you’re already a PHP IoC expert, please feel free to skip to the Ruby section.
There are a number of Inversion of Control Containers, IoC Containers for short, out in the wild jungles of open source software.
I’ll show you just how difficult unit testing without IoC can be. In this example we’ll be using the excellent GuzzleHttp Packages as our client to make HTTP calls to the Open Weather API.
The goal of this class is to expose a method weatherIn()
that accepts an argument of a city name. It will retrieve the current weather in that city from a free Weather API and return the response.
Let’s go ahead and create a simple, but untestable PHP version of this Weather Forecaster:
This simple class will allow us to get the current weather of any city listed in the OpenWeatherApi:
However if we wanted to unit test this class, we’ll quickly find out that we will be making an API call to the OpenWeatherApi every single time we run our test suite.
One way to mitigate this problem is by mocking the GuzzleHttp Client and returning the response.
However, because the GuzzleClient is initialized in the constructor we’re unable to mock it.
The Solution
In general, the less your classes know to function the better. This lesson touches on the concept of the Single Responsiblity Principle which basically states:
A class should be good at one thing.
Our Forecaster
class is doing a so-so job at doing 2 things:
- Create and store a GuzzleHttp client with a specific base URL
- Send an API request with the stored GuzzleHttp client and return the decoded JSON results
Our Forecaster
class should only be concerned with weather things right? Intializing and configuring an HTTP client shouldn’t be one of them. The creation and configuration of dependencies is were Inversion of Control Containers shine.
Refactoring Using an Inversion of Control Container
All major PHP frameworks include an IoC Container. It’s not a construct of the language but it’s a pattern that’s used to help us separate the creation and configuration of dependencies.
Whew that’s a lot of big words so I’ll just show you with a reasonable example.
Because we’re not swearing to any PHP framework in this book - I’ll turn to some of the highest quality PHP packages available. I’m speaking of the League of Extraordinary Packages.
These packages are awesome because they’re held to a very high standard. A few of the requirements to have a package listed:
- Write unit tests. Aim for at least 80% coverage in version 1.
- Use Semantic Versioning to manage version numbers.
- Use a vendor namespace (
League
in our case) for PSR-4 autoloading. Shove code in a src folder.
etc.
You can read the other quality standards at PHP League’s Quality page
One such package in this repository includes a package simply called Container
The Container package allows us to register and retrieve services:
Think of IoC Containers as just simple key => value stores. We store a class or Closure in the container and assign it a name so we can retrieve it later.
The benefits are threefold:
- Easier unit testing because we can actually mock dependencies
- Easier composition of classes
- Code reuse becomes possible
This will become apparent when we refactor our Forecaster
Injecting the GuzzleHttp Client as a Dependency
To refactor our Forecaster
class to know less and still be able to function as expected, we’ll move the creation of the GuzzleHttp Client outside of the class.
First thing’s first, let’s pass the GuzzleHttp client as an constructor argument:
Now, our Forecaster
has no idea what $guzzle
is, it only knows that there is a get()
method defined on it. Congratulations! We can now unit test our Forecaster
class because we can pass in a mocked version of Guzzle:
That’s great and all, but that means whenever we want to use this WeatherApi
class in our code, we have to copy and paste the GuzzleHttp creation & configuration first right?
That’s just inefficent and not DRY. In the next section I’ll show you the final piece to understanding how to use the IoC Container to clean up this mess.
How to use an IoC Container to Leverage Dependency Injection
Now for the final piece - using a real IoC Container to abstract away the dependency injection required so we can unit test our PHP code.
Whew say that 5 times fast. Anyway, it’s a lot of terminology but I assure you it’s not as complicated as it sounds.
As you read before, the IoC Container is just a key => store container. You can think of it as an array of services. A service is just a piece of functionality that your application will use to perform some business logic.
In this example we’ll turn our WeatherApi into a service stored in a Container.
Let’s leverage the Container’s FactoryClosures which act identically to Laravel’s IoC service definitions.
IoC Containers in Ruby
Welp, I hate to break it to you. IoC Containers aren’t really practiced in Ruby. I tried searching and searching for a definitive reason why, it bothered me for the longest time.
Because of the rigidness of static languages like PHP and Java, the IoC is pivotal. Without it we we would have much more difficulty unit testing our code.
Let’s start with a Ruby-ified version of our first naive Forecaster
implementation, without the Dependency Injection.
First major difference is that Ruby doesn’t have a GuzzleHttp gem persay. They have a few swell HTTP Clients. For simplicity I’ll use the rest-client gem.
require 'json'
class Forecaster
attr_accessor :client
def initialize
@client = RestClient::Resource.new('https://api.openweathermap.org/data/2.5/weather')
end
def weather_in(city)
response = @client.get(params: {
q: city,
appId: 'INSERT YOUR APP ID HERE'
})
JSON.parse(response)
end
end
forecaster = Forecaster.new
weather = forecaster.weather_in('West Palm Beach, FL')
# re-encoding the JSON so we can see it in it's glorious string format
puts JSON.stringify(weather)
=> {"coord":{"lon":-0.13,"lat":51.51},"weather":[{"id":300,"main":"Drizzle","description":"light intensity drizzle","icon":"09d"}],"base":"stations","main":{"temp":280.32,"pressure":1012,"humidity":81,"temp_min":279.15,"temp_max":281.15},"visibility":10000,"wind":{"speed":4.1,"deg":80},"clouds":{"all":90},"dt":1485789600,"sys":{"type":1,"id":5091,"message":0.0103,"country":"GB","sunrise":1485762037,"sunset":1485794875},"id":2643743,"name":"London","cod":200}
Voila, we have a simple version of the Weather API without an IoC Container performing Dependency Injection.
In Ruby, Everything is Testable
So finally we get to dive into the practical benefits of the flexibility Ruby gives us.
Remember in Ruby that we have the ability to override any public method? This special power includes the ability to override a constructor at whim. How does this help our situation?
Well, let’s take a look into the initialize
function of our WeatherApi
and how it sets up the rest-client
instance:
# looking inside of the Forecaster class
def initialize
@client = RestClient::Resource.new('https://api.openweathermap.org/data/2.5/weather')
end
In PHP, we were forced to pass the HTTP client as an argument to the constructor. But in Ruby we can just override that particular constructor:
# Creating a mock "double" which is essentially the same thing as a Mockery::mock() object
mock = double(RestClient::Resource)
# set the exceptation that the RestClient::Resource will be initialized and override the returning value
allow(RestClient::Resource).to(
receive(:new).with('https://api.openweathermap.org/data/2.5/weather').and_return(mock))
Volia, we have mocked RestClient::Resource
without any special Container needed.
This is a sneak peak of the testing section