TDD Patterns: Humble Object
Table of Contents
We all know that there are different design patterns. They are all quite trivial to learn, but, the trick lies in applying them. When should we use this or that pattern and will that help in making our code better and cleaner. Well, tests are code as well and, you guessed it, there are some testing patterns that are around for a while.
Today, we will take a look at one of them. It is a useful one and also it has an interesting name - the Humble Object Pattern.
The problem #
Think about this. You are building an API client/wrapper. You read the documentation, you understand the model and the intent of the API and how everything is composed. You start writing your code, keeping it in small chunks (i.e. classes). You are a responsible programmer, you want to have your code well documented and tested. All of this is nice. And then, you get to this part where you start mocking your HTTP calls and stuff easily gets messy.
How can we avoid this? Is there a better way?
Humble Object #
Let us take a detour. A bit of history first. I think that Uncle Bob has a really good definition of the Humble Object pattern in Episode 23 of his Clean Code videos:
This pattern is applied at the boundaries of the system, where things are often difficult to test, in order to make them more testable. We accomplish the pattern by reducing the logic close to the boundary, making the code close to the boundary so humble that it doesn't need to be tested. The exctacted logic is moved into another class, decoupled from the boundary which makes it testable.
- Robert C. Martin
In case you find the definition confusing, let’s tear it apart. First, the boundaries. When one says boundaries, it means that the person is referring to the part of the system that communicates with other software that is not written by you, but your software is dependent on it. For example, let’s say your software creates cron jobs. The boundary lies beween the software that will call the cronjob system command and the operating system.
So, this means when using the Humble Object pattern, we extract as much logic as we can from the boundary class(es), thus making them humble. The extracted logic will be moved to another class, which will be easily testable. On the other hand, the humble code will be dependant on the extracted class, but testing it won’t be necessary, because it doesn’t hold any business logic.
Usually, when explaining the humble object, people use GUIs or async code as examples. We won’t go down that path today. Let’s try finding an example which we might run into more frequently.
Back to the problem #
Now, that we undestand the motivation and the theory behind this pattern, let’s continue with aforementioned design problem.
Tackling any programming problem, in my opinion, is best understood via some code. Here’s an example. We will make a tiny API wrapper of the REST Countries API, more specifically, the World Capitals API.
require 'net/http'
require 'json'
class CapitalsClient
API_ENDPOINT = "https://restcountries.eu/rest/v1/capital/"
def self.find(capital_name)
uri = URI(API_ENDPOINT + capital_name)
response = Net::HTTP.get(uri)
result = JSON.parse(response).first
currencies = result['currencies'].map {|currency| Currency.new(currency) }
Country.new({ name: result["name"],
capital: result["capital"],
region: result["region"],
population: result["population"],
latitude: result["latlng"][0],
longitude: result["latlng"][1],
native_name: result["nativeName"],
currencies: currencies
})
end
end
The CapitalsClient
will issue a GET request to the API endpoint, get the
result and build a Country
object from the results. This is what the JSON
result looks like for London, UK:
[
{
"name": "United Kingdom",
"capital": "London",
"altSpellings": [
"GB",
"UK",
"Great Britain"
],
"relevance": "2.5",
"region": "Europe",
"subregion": "Northern Europe",
"translations": {
"de": "Vereinigtes Königreich",
"es": "Reino Unido",
"fr": "Royaume-Uni",
"ja": "イギリス",
"it": "Regno Unito"
},
"population": 64105654,
"latlng": [
54,
-2
],
"demonym": "British",
"area": 242900,
"gini": 34,
"timezones": [
"UTC−08:00",
"UTC−05:00",
"UTC−04:00",
"UTC−03:00",
"UTC−02:00",
"UTC",
"UTC+01:00",
"UTC+02:00",
"UTC+06:00"
],
"borders": [
"IRL"
],
"nativeName": "United Kingdom",
"callingCodes": [
"44"
],
"topLevelDomain": [
".uk"
],
"alpha2Code": "GB",
"alpha3Code": "GBR",
"currencies": [
"GBP"
],
"languages": [
"en"
]
}
]
For completeness sake, let’s see the Currency
and Country
classes.
class Currency
def initialize code
@code = code
end
def to_s
@code
end
end
class Country
attr_reader :name, :capital, :region, :population, :latitude, :longitude,
:native_name, :currencies
def initialize attrs
@name = attrs.fetch("name",nil)
@capital = attrs.fetch("capital",nil)
@region = attrs.fetch("region",nil)
@population = attrs.fetch("population",nil)
@latitude = attrs.fetch("latlng",[]).first
@longitude = attrs.fetch("latlng",[]).last
@native_name = attrs.fetch("nativeName",nil)
@currencies = attrs.fetch("currencies", [])
end
end
Now, let’s revisit the CountryClient
class. We’ve all done this - we fetch
the JSON, parse it and build the currencies and country object(s). Now, testing
is interesting.
First, we’ll need to stub the GET request using
Webmock and assert on the Country
object that we receive as a result of the CapitalsClient::find
method.
Another approach is to use VCR and record the request going out and replay it
whenever needed.
require 'minitest/autorun'
require 'webmock/minitest'
class CountryClientTest < Minitest::Test
def test_client_fetches_countries
response = %Q{
[
{
"name": "United Kingdom",
"capital": "London",
"region": "Europe",
"population": 64105654,
"latlng": [54, -2],
"nativeName": "United Kingdom"
}
]
}
stub_request(:get, CountryClient::API_ENDPOINT + "London").to_return(body: response)
country = CapitalsClient.find("London")
assert_equal "London", country.name
assert_equal "United Kingdom", country.name
assert_equal "Europe", country.region
end
end
Now, this works, it’s fine. But, what exactly are we testing here? I am sure we
have all done this multiple times. Look at the test class name -
CountryClientTest
. We should be testing the client, not setting assertions
on the country object. The CountryClient
acts like a factory, not like an
API client.
Also, think about this - stubbing, although it looks fine, it’s basically isolation. While we cannot completely rely on pulling real data from the API for each of our tests, we shouldn’t also go overboard with it.
While one can argue that stubbing external services is all good, what would the case be if you used a library that was actually fetching the data and building it for you? What would you test if the wrapper was made by someone else and you are using it in a Rails app? You could go on and stub the library, but you have no idea if the API and/or the wrapper had any changes made to them.
But, let’s take a step back. How can we apply the humble object pattern here?
Applying the pattern #
If you remember, the pattern states that we need to extract most of the logic near the boundaries of the system, so the code on the boundary itself is so humble, it doesn’t need to be tested. But, the humble object should be dependent on the extracted code.
Let’s try to refactor our code by doing exactly that.
First, the CountryClient
. It sure does more than it should. Let’s make it
humble. The first step would be to make it an API client. Exactly that,
nothing more or less. This means that it will only issue HTTP GET to the API.
Hint: think of the Single-responsibility principle.
require 'net/http'
class CapitalsClient
API_ENDPOINT = "https://restcountries.eu/rest/v1/capital/"
def self.find(capital_name)
uri = URI(API_ENDPOINT + capital_name)
Net::HTTP.get(uri)
end
end
That’s it. Again, CapitalsClient
just sends the request and it returns it’s
response. Now, the extracted logic.
Since the code that we extracted was actually building the Country
object, we
can create a CountryBuilder
class out of it:
require 'json'
class CountryBuilder
def self.build json
result = JSON.parse(response).first
currencies = result['currencies'].map {|currency| Currency.new(currency) }
Country.new({ name: result["name"],
capital: result["capital"],
region: result["region"],
population: result["population"],
latitude: result["latlng"][0],
longitude: result["latlng"][1],
native_name: result["nativeName"],
currencies: currencies
})
end
end
The CountryBuilder.build
method will receive the JSON and build the Country
object on it’s own. The next, obvious step, is to test this class. If you look
at the test we wrote earlier, you can notice that the test is 90% done, it just
needs some tweaking.
require 'minitest/autorun'
class CountryBuilderTest < Minitest::Test
def test_build_builds_the_country
response = %Q{
[
{
"name": "United Kingdom",
"capital": "London",
"region": "Europe",
"population": 64105654,
"latlng": [54, -2],
"nativeName": "United Kingdom"
}
]
}
country = CountryBuilder.build(response)
# Sanity check
assert_equal Country, country.class
# Useful assertions
assert_equal "London", country.name
assert_equal "United Kingdom", country.name
assert_equal "Europe", country.region
end
end
The key differences in the new and old test is that the new one is missing the
Webmock request stub. Also, we are testing the builder class, which does what
it should - receives the response as a JSON, builds an object of a class and
returns it. But, what happens now to the CapitalsClient
? Well, nothing too
complicated. If you remember, the pattern states that the humble object should
depend on the extracted code.
If you look at the code below, it should all make sense:
require 'net/http'
class CapitalsClient
API_ENDPOINT = "https://restcountries.eu/rest/v1/capital/"
def self.find(capital_name)
uri = URI(API_ENDPOINT + capital_name)
response = Net::HTTP.get(uri)
CountryBuilder.build(response)
end
end
So, we add the dependency, making CapitalsClient
use CountryBuilder
to
create the country out of the JSON payload.
And, what about testing CapitalsClient
? Well, we don’t really need to test
it. Even if we wanted to test it, we could only write a test with a test spy
that would expect CountryBuilder.build
to be called. But, how useful is that
test? If we wrote it, we would tie our test to the implementation of the
production code. This means that if the implementation of this method changes
in the future, but it’s output does not, our tests will fail although we
haven’t broken our code.
Outro #
As you can see, although very simple, this humble pattern can make a difference when we want to leave out heavy stubbing to external services, APIs or interfaces and just focus on our code where “the magic” happens. Also, at least for me, this type of refactor comes very natural in these situations. But, knowing the pattern guides us towards making only one entry point to our code (or, dependency).