Testing Objects with a Functional Mindset

Joël Quenneville

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.

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.

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.

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

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!

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 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.