In this series, we've discussed several topics already. We talked about persistence and time, the filesystem and randomness. The conclusion for all these areas: whenever you want to "mock" these things, you may look for a solution at the level of programming tools used (use database or filesystem abstraction library, replace built-in PHP functions, etc.). But the better solution is always: add your own abstraction. Start by introducing your own interface (including custom return types), which describes exactly what you need. Then mock this interface freely in your application. But also provide an implementation for it, which uses "the real thing", and write an integration test for just this class.
The same really is true for the network. You don't want your unit tests to rely on a network connection, or a specific web service to be up and running. So, you override PHP's curl_exec()
function. Or, if your code uses Guzzle, you inject its Mock Handler to by-pass part of its real-life behavior. The smarter solution again is to introduce your own interface, with its own implementation, and its own integration test. Then you can prove that this implementation is a faithful implementation of the interface (contract) you defined. And it allows you to mock at a much more meaningful level than just replacing a "real" HTTP response with a recorded one.
Though this solution would be quite far from traditional mocking I thought it would be interesting to write a bit more about it, since there's also a lot to say. It does require a proper example though. Let's say you're writing (and testing) a piece of financial calculation code where you're doing a bit of currency-conversion as well. You need a conversion rate, but your application doesn't know about actual rates, so you need to reach out to some web service that keeps track of these rates. Let's say, you make a call to "exchangerates.com":
# ...
$response = file_get_contents('https://exchangerates.com/?from=USD&to=EUR&date=2018-03-18')
$data = json_decode($response);
$exchangeRate = (float)$data->rate ?? 1;
# use the exchange rate for the actual calculation
# ...
Yes, this is horrible. Testing this code and "mocking" the network call is only one of our problems. We have to deal with broken connections and responses, and by the way, this code doesn't even take into account most of the other things that could go wrong. Code like this that connects with "the big bad world" requires a bigger safety net.
The first thing we should do is (as always) introduce an interface for fetching an exchange rate:
interface ExchangeRateService
{
public function getFor(string $from, string $to, DateTimeImmutable $date): float
}
We could at least move all that ugly code and stick it in a class implementing this interface. Such is the merit of setting up a "facade", which "provides a simplified interface to a larger body of code". This is convenient, and it allows client code to use this interface for mocking. At the same time though, we're hiding the fact that we're making a network call, and that things can go wrong with that in numerous ways.
Implement an Anti-Corruption Layer
The first thing we can and should do is protect ourselves from the bad (data) model which the external service uses. We may have a beautiful model, with great encapsulation, and intention-revealing interfaces. If we'd have to follow the weird rules of the external service, our model risks being "corrupted".
That's what Domain-Driven Design's "Anti-Corruption Layer" (ACL - a bit of a confusing name) is meant for: we are encouraged to create our own models and use them as a layer in front of an external service's source of data. In our case, the interface we introduced was a rather simple one, one that doesn't allow for proper encapsulation. And because of the use of primitive types, there certainly isn't a place for a good and useful API. Due to a quirk in the external service I didn't mention yet, if one of the currencies is EUR, it always needs to be provided as the second ($to
argument).
It'll be a perfect opportunity for an ACL. Instead of dealing with an exchange rate as a rather imprecise float
type variable, we may want to define it as an integer and an explicit precision. And instead of working with DateTimeImmutable
, we'd be better off modelling the date to be exactly what we need, and encode this knowledge in a class called ExchangeDate
. Also, not every string is a currency, so we'd better define a class for it too.
We'd end up with an improve interface:
interface ExchangeRateService
{
public function getFor(Currency $from, Currency $to, ExchangeDate $date): ExchangeRate
}
final class ExchangeRate
{
public function __construct(
Currency $from,
Currency $to,
ExchangeDate $date,
int $rate,
int $precision
) {
// ...
}
}
The implementation for the service could easily work around that quirk about the order of from/to currencies. It would require swapping the arguments, then calculating the inverse of the exchange rate (for the purpose of which we could easily add a modifier method - invert()
to our ExchangeRate
object):
public function getFor(Currency $from, Currency $to, ExchangeDate $date)
{
if ($from->equals(Currency::EUR())) {
return $this->fetchExchangeRate(
$to,
$from,
$date
)->invert()
}
return $this->fetchExchangeRate(
$from,
$to,
$date
);
}
private function fetchExchangeRate(
Currency $from,
Currency $to,
ExchangeDate $date
): ExchangeRate
{
// ...
}
Inverting the runtime dependency
Even though step 1 - adding an ACL - is a big improvement for our model and related services, the next best thing for our application would be if we wouldn't have to make the external request at all. We'd want to invert the runtime dependency on the remote service, and there's several things we can do to achieve that (if you're interested in the topic of autonomous services, check out my book).
Different situations will require different solutions, but in this case, the most "obvious" solution would be to somehow make sure that your application already knows the answer to the question it was going to ask to the remote service. You could accomplish this by making daily background calls to the exchange rate service, and keep a local list of exchange rates for any currency that your application supports.
The good thing is, if you have developed a nice ACL, you can keep using it, whether or not the data will be fetched at runtime, all has already been fetched in a background job.
Conclusion
This concludes the series about "mocking at architectural boundaries". In all cases, the advice is to introduce your own abstractions. When it comes to dealing with a network connection, instead of asking the question "How can I mock the network?", you should be considering how you could add a proper protective (model) layer around it, and ideally, how you can prevent making the network call at all.
I'd have loved to see a more simple example with actual testing code, before/after both in user land and in test code.
I think that would be nice, but unfortunately I don't have time for it; writing a blog post every week already takes quite some time. I was hoping that the examples would be good hints for coming up with a full implementation. If you have specific questions, please ask them here!