---
title: Simplifying Tests by Extracting Side-Effects
teaser: Test-driven development, object-oriented design, and functional programming
  converge on some similar ideas.
tags: web,ruby,tdd,testing,functional programming,good code
author: Joël Quenneville
published_on: 2021-02-18
---

Functional programming [gives us a mental model] that can be helpful when looking
at unit testing our object-oriented code. In particular, it gives us two terms:

1. **Pure functions** - these are deterministic methods with explicit inputs and
   outputs (e.g. no implicit dependency on the system clock). This kind of
   method is the easiest to test.
2. **Side-effects** - Any source of inputs or outputs for a method that are not
   part of the arguments or return value. Examples include calls to `Time.now`,
   or writing the output to a file. These types of methods are
   non-deterministic, which makes them trickier to test and usually involves
   some mocking or stubbing.

[Test-driven development][tdd], [object-oriented design][ood], and [functional
programming][fp] converge on some similar ideas. One of these is _turning our
side-effectful methods into pure functions_.

[gives us a mental model]: https://thoughtbot.com/blog/functional-viewpoints-on-testing-objectoriented-code
[tdd]: https://thoughtbot.com/blog/tags/testing
[ood]: https://thoughtbot.com/upcase/clean-code
[fp]: https://thoughtbot.com/blog/tags/functional-programming

## Passing in the side-effect

> Pain in your tests should push you to change the design of objects being
> tested
>
> -- TDD mantra

Consider the following innocent-looking piece of code. It defines a token that
expires 24 hours after creation.

```ruby
class Token
  attr_reader :created_at

  def initialize
    @created_at = Time.now
  end

  def expires_at
    created_at + 24.hours
  end
end
```

This is actually harder to test than might first be apparent because the
dependency on `Time.now` makes the code non-deterministic. We are going to need
a way to stub the system clock. Either manually, with a [gem], or with
[framework helpers]. If we want to test behavior in the past or future, we'll
need some form of time-travel.

```ruby
it "sets expiry in 24 hours" do
  # this could also be done with a time-travelling helper
  # I'm showing the explicit stub here for illustrative purposes
  allow(Time).to receive(:now).and_return(Time.new(2020, 11, 2))
  token = Token.new(created_at: created_at)

  expect(token.expires_at).to eq Time.new(2020, 11, 3)
end
```

So how might we change a design to avoid this awkward stubbing? Looking at the
problem through the functional programming mental model, we might rephrase the
question as _"how can we turn this side-effectful method into a pure one?"_.

The classic solution to this problem is to **turn implicit dependencies into
explicit ones by passing them in as arguments**.

The dependency on `Time.now` in the constructor of our `Token` object is a
side-effect. Consider the following small change to the constructor: instead of
calculating `Time.now` it takes the created at time as an argument.

```ruby
def initialize(created_at:)
  @created_at = created_at
end
```

There is no longer any dependency on the system clock. The `Token` constructor
and `#expires_at` methods are now pure functions. This allows us to write tests
without any stubbing:

```ruby
it "sets expiry in 24 hours" do
  created_at = Time.new(2020, 11, 2)
  token = Token.new(created_at: created_at)

  expect(token.expires_at).to eq Time.new(2020, 11, 3)
end
```

As bonus, we are now able to create tokens that started in the past or that will
start in the future.

[gem]: https://github.com/travisjeffery/timecop
[framework helpers]: https://api.rubyonrails.org/v6.0.3/classes/ActiveSupport/Testing/TimeHelpers.html

## Passing in IO

"Passing in the side-effect" can be used in all sorts of situations. I'm
particularly fond of this technique when [dealing with IO]. For example, instead
of making raw calls to `Kernel#puts`, I'll pass in my own IO object:

```ruby
class MyTask
  def initialize(io)
    @io = io
  end

  def write_lines(lines)
    @io.puts lines.joins("\n")
  end
end
```

This is much easier to test than attempting to stub `$stdout` because we can
pass in our own IO object and assert directly on it:

```ruby
it "puts each line to the given IO" do
  io = StringIO.new
  task = MyTask.new(io)

  task.write_lines ["1", "2", "3"]

  expect(io.rewind.read).to eq("1\n2\n3\n")
end
```

As a bonus, `MyTask` can now write to stdout, a file, or any other IO we pass
it.

[dealing with IO]: https://thoughtbot.com/blog/io-in-ruby#putting-it-all-together

## Isolating side-effects

This is well and good for simple side-effects like time and IO, but what about
code where side-effects are all over the code such as when integrating a
third-party API?

This is where we get to put our OO designer hats on and try to separate code
that is side-effectful (in this case, network code) from everything else. We
don't always have to pull the side-effects out. Sometime we can do the opposite
and find some behavior that can be pulled into a "pure" object. For example if
we're sending JSON to this API, it might make sense to extract a serializer
object. We might also pull out some lightweight [value objects] out. Both of
these have no side-effects and can be easily unit-tested.

That's a win but we can do better. Notice how the reason this scenario is more
difficult is because side effects are all over the code? This is a code smell.
It's a violation of the [single responsibility principle], and making changes to
it will likely cause [shotgun surgery].

When I test-drive API integration code, I often find myself breaking out two
classes:

1. A `Driver` that isolates all the network code in one place. This is
   particularly nice if I have to make multiple network calls for one task like
   conditionally refreshing an OAuth token before calling an endpoint.
2. A `Client` that handles the actual business logic.

By coalescing all the network code into a single `Driver` object, I've extracted
all the side-effects into one place which allows me to pass them into the
`Client`. Now my client tests don't need to stub the network anymore.

Since Ruby is a [duck-typed] language, I can pass in a test double that
pretends to be a driver. This allows me to pass in a "driver" that always
succeeds, always fails, returns a particular payload, or whatever I need for the
particular scenario that I'm testing.

So what happens if you take this to an extreme? Eventually, all your
side-effects get pushed to the edges of your system. [Gary Bernhardt] explores
this in his classic ["Functional Core, Imperative Shell"]. You may hear this
pattern referred to as "bring your own IO" in other programming communities,
as can be seen in the [Python h11 library].

[value objects]: https://thoughtbot.com/upcase/videos/value-objects
[shotgun surgery]: https://refactoring.guru/smells/shotgun-surgery
[single responsibility principle]: https://thoughtbot.com/upcase/videos/single-responsibility-principle
[duck-typed]: https://thoughtbot.com/blog/back-to-basics-polymorphism-and-ruby#duck-typing
[Gary Bernhardt]: https://twitter.com/garybernhardt
["Functional Core, Imperative Shell"]: https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell
[Python h11 library]: https://github.com/python-hyper/h11

## Same problem, multiple perspectives

If you come from a test-driven development background, modifying your design by
extracting objects to avoid awkward stubs may already be part of your practice.

If you do a lot of object-oriented design, you may find many design principles
also lead you to the same place. Coalescing similar code together and extracting
objects around them is the [single responsibility principle]. Passing
dependencies in as arguments rather than hard-coding them is [dependency
injection]. Even the concept of isolating your side-effects from your pure code
may sound familiar as [command-query separation].

This is because TDD, OO design, and FP are all looking to create code with _low
coupling_. Tight coupling is what makes tests hard to write, and
side-effects/implicit dependencies are a particularly pernicious form of
coupling. Low coupling makes it much easier to [test in isolation].

Having different mental models in your toolbox allows you to analyze the same
problem from multiple perspectives. Each can give you different insights to
better understand the issue at hand and help you come to the best solution.

[dependency injection]: https://thoughtbot.com/upcase/videos/dependency-inversion-principle
[command-query separation]: https://martinfowler.com/bliki/CommandQuerySeparation.html
[test in isolation]: https://thoughtbot.com/upcase/videos/testing-in-isolation
