Why mocking is a good idea

... when done right

Posted on October 29, 2021

Someone wrote a recent post debunking what the author thinks are Mocks. I say “thinks” because actually, what the author describes are not what Mock objects really are or how they were thought to be used, as this post demonstrates.

What are Mock Objects really?

The history of the Mock object pattern for Test-Driven Development is explained by Steve Freeman its inventor. He wrotes, with Nat Pryce, the book Growing Object-Oriented Software Guided by Test which is a must read even if one does only functional programming. There’s also a shorter OOPSLA 2004 paper and an XP 2000 paper. The very same idea was also exposed in the context of refactoring legacy code by Michael Feathers in his seminal Working Effectively with Legacy Code book which is another must read, even if one does functional programming.

The key insight about Mock objects dawned on me when I realised these were really Design tools, much like one harvests most of the benefits of TDD when she conceives tests as a force to drive software design and not merely as a safety net against regression. In other words, one does not use Mock objects, or mock interfaces, only to replace a cumbersome dependency, but rather mocks roles that emerge from the needs of some piece or component of the system, to express the expectations of this component regarding some dependency without having to depend on the implementation details. Mocks are used to design interfaces between moving parts of the system, or to let seams appear as M.Feathers names those.

The proof is in the pudding

Here is a concrete example drawn from my current project, Hydra, implemented in Haskell. The Hydra node we are building needs to interact with a Chain component, both to submit transactions and to receive them. Instead of having to depend on the somewhat complicated implementation details of a real Cardano node, we instead defined an interface which expresses in a concise and implementation independent way the information we send and receive.

Because we are using a functional language, this interface is expressed as a function which itself takes other functions (callbacks), a recurring pattern we documented in an Architectural Decision Record:

type ChainComponent tx m a = ChainCallback tx m -> (Chain tx m -> m a) -> m a

where

type ChainCallback tx m = OnChainTx tx -> m ()

newtype Chain tx m = Chain  { postTx :: MonadThrow m => PostChainTx tx -> m () }

The details of the messages we are sending and receiving are defined as two separate data types, one representing outbound transactions, eg. transactions we’ll post to the chain:

data PostChainTx tx
  = InitTx {headParameters :: HeadParameters}
  | CommitTx {party :: Party, committed :: Utxo tx}
  | AbortTx {utxo :: Utxo tx}
  | CollectComTx {utxo :: Utxo tx}
  | CloseTx {snapshot :: Snapshot tx}
  | ContestTx {snapshot :: Snapshot tx}
  | FanoutTx {utxo :: Utxo tx}

and the other representing inbound messages, eg. transactions and errors observed on the chain:

data OnChainTx tx
  = OnInitTx {contestationPeriod :: ContestationPeriod, parties :: [Party]}
  | OnCommitTx {party :: Party, committed :: Utxo tx}
  | OnAbortTx
  | OnCollectComTx
  | OnCloseTx {contestationDeadline :: UTCTime, snapshotNumber :: SnapshotNumber}
  | OnContestTx
  | OnFanoutTx
  | PostTxFailed

Had we coded in an Object-Oriented language, or used a final tagless encoding, these would have been expressed as two separate interfaces with one method for each type. The important point here is that we are in control of this interface, we define the patterns of interactions with the external system we depend on and abstract away all the nitty-gritty details a dependency on a concrete implementation would entail. Our system is now loosely coupled to the other system as we have separated concerns.

This has allowed us to focus on the core functionality of our software, to build a complete implementation of the Hydra off-chain protocol, demonstrate a network of Hydra nodes and have automated end-to-end tests all without the hassle of dealing with a full Cardano node implementation. Instead, we have several Mock implementations of the Chain interface described above suitable for various use cases: - A very fast in-process mock for Behaviour-driven testing of Hydra cluster, - A 0MQ-based implementation that allows us to spin-up a cluster of Hydra nodes, completely mocking an external chain.

Note that: - We are not side-stepping the integration problem as demonstrated by the fact we are also testing interaction with Cardano node, and we definitely will enhance our End-to-End tests to “Close the loop” once the Chain tests demonstrate the concrete implementation of our abstract transactions work correctly on a real Cardano network, - We have been careful to Mock types we own in order to not fall into the trap of relying on a dumbed down and probably wrong implementation of some system we depend on.

There is more to it

This whole idea has been applied in a couple other areas of the system, most notably the Network interface: Here again we express some requirements from the point of view of the Hydra node, letting those emerge from tests we write.

While it does not respect the Mock types you own mantra, we have also used this technique to great profit leveraging the io-sim and io-classes, as exposed in another ADR. This has allowed us to test-drive the development of the protocol in an outside-in way, expressing expected observable behaviour in a cluster of nodes, in a safe and fast way, as pure functions.

Of course, this is a dangerous path to tread and we need to also run tests with the real Multi-threaded Runtime System to ensure proper coverage, like the End-to-end tests I already talked about and load testing.

Conclusion

I hope this post managed to convince the reader that using Mock objects is actually a good idea as soon as one embraces it the way it’s been intended to be used, namely as a Test-driving technique also called London School TDD, and not as a mere technical artifact to ease testing after the fact. As I already advocated a while ago TDD has a fractal dimension: It can, and in my opinion must, be applied at all stages of a system’s development and at all level of abstractions.

“Mock objects” is just a name for the technique that lets one work outside-in, precisely and formally (in code) expressing and testing each component first in isolation, but within a broader context: As soon as a concrete, production-ready implementation of an interface is ready, it should be integrated in the higher level tests. This is also something we wrote down in an ADR on Testing strategy as it deeply impacts the architecture of the system.