---
title: Testing Objects with a Functional Mindset
teaser: Some ideas from functional programming can help us improve the unit tests
  for our object-oriented code.
tags: web,functional programming,testing,good code,ruby
author: Joël Quenneville
published_on: 2021-01-19
---

When unit testing our Object-Oriented (OO) code, some methods are easy to test
while others are hard. Functional programming gives us a mental model to help
understand why: **pure functions** and **side-effects**.

## Easy tests

Some methods are super easy to test. You set up an object, call the method, and
expect a given result. This sort of test makes TDD a breeze.

```ruby
it "uppercases the string" do
  expect("abc".upcase).to eq "ABC"
end
```

Have you ever wondered _why_ these methods are so easy to test? It's because all
the inputs and outputs are above board. There are no implicit inputs like the
system clock. Functional programmers might call such a method a [pure function].

Because we have direct references to **explicit inputs**, we can control for the
various happy path and edge case scenarios just by changing the arguments we
pass in during setup. Having an **explicit output** as the return value means
we have a reference we can easily assert on. These two things are a big part of
what makes a test "easy" to write rather than "hard".

An additional benefit of "pure" methods is that they are **deterministic**.
Given an input, they always give back the same output. This quality makes it
easy to write tests that are consistent, which in turn gives us more confidence
in our test suite.

## Side-effects

Contrast this with a function that might rely on data from the system clock or
the network in addition to its arguments. Or maybe one that writes to the file
system in addition to returning a value. Functional programmers often refer to
these as **side-effects**. Without stubbing, these tests are
**non-deterministic**. Their results depend on the time of day or the state of a
network. Non-determinism in a test suite leads to intermittent failures and a
loss of confidence.

Additionally, because these extra inputs and outputs are implicit, we *don't
have any direct references* to them in our tests which makes it harder to assert
on them or to vary them for different scenarios.

[pure function]: http://blog.jenkster.com/2015/12/what-is-functional-programming.html

## Stubbing

But wait! We have tools to deal with these side-effects! **Stubbing** allows you
to force canned responses from outside systems. In the best cases this is
relatively effortless such as when using [Timecop] or [Rails' time testing
helpers], but it can also get painful quite fast.

```ruby
it "sets expiry in 24 hours" do
  freeze_time do
    token = Token.new

    expect(token.expires_at).to eq 24.hours.from_now
  end
end
```

[Timecop]: https://github.com/travisjeffery/timecop
[Rails' time testing helpers]: https://api.rubyonrails.org/v6.0.3/classes/ActiveSupport/Testing/TimeHelpers.html

## Downsides of stubbing

In many cases, stubbing can involve a lot of awkward setup. It's also easy to
get wrong. If you [stub things incorrectly], you can easily end up writing a
**tautological test** which always succeeds no matter what. This is worse than
useless. Because of these issues, stubbing has a bad name with many testers.

> Never stub the system under test
>
> ~ Testing axiom

Make sure that you only stub side-effects!

[stub things incorrectly]: https://thoughtbot.com/blog/don-t-stub-the-system-under-test

## Conclusion

Functional programming gives us a mental model of thinking in terms of pure
functions and side effects. Keeping these concepts in mind helps us better
analyse the way that we test our object-oriented code. Code that has
side-effects will likely need stubbing while pure functions are much easier to
test since they are deterministic and give us easy references to their inputs
and outputs.

There's also a third way: [refactoring your code to eliminate or extract the side
effects][extract side-effects] and turn the system under test into pure functions. It turns out this
idea lies at a fascinating nexus between test-driven development,
object-oriented design, and functional programming.

[extract side-effects]: https://thoughtbot.com/blog/simplify-tests-by-extracting-side-effects
