A pure Test Spy by Mark Seemann
Ad-hoc Test Spies can be implemented in Haskell using the Writer monad.
In a previous article on state-based testing in Haskell, I made the following throw-away statement:
"you could write an ad-hoc Mock using, for example, the Writer monad"In that article, I didn't pursue that thought, since the theme was another. Instead, I'll expand on it here.
Test Double continuum #
More than a decade ago, I wrote an MSDN Magazine article called Exploring The Continuum Of Test Doubles. It was in the September 2007 issue, and you can also download the entire issue as a single file and read the article offline, should you want to.
In the article, I made the argument that the classification of Test Doubles presented in the excellent xUnit Test Patterns should be thought of more as a continuum with vague and fuzzy transitions, rather than discrete categories.
This figure appeared in the original article. Given that the entire MSDN Magazine issue is available for free, and that I'm the original author of the article, I consider it fair use to repeat it here.
The point is that it's not always clear whether a Test Double is, say, a Mock, or a Spy. What I'll show you in this article is closer to a Test Spy than to a Mock, but since the distinction is blurred anyway, I think that I can get away with it.
Test Spy #
xUnit Test Patterns defines a Test Spy as a Test Double that captures "the indirect output calls made to another component by the SUT [System Under Test] for later verification by the test." When, as shown in a previous article, you use Mock<T>.Verify
to assert than an interaction took place, you're using the Test Double more as a Spy than a Mock:
repoTD.Verify(r => r.Update(user));
Strictly speaking, a Mock is a Test Double that immediately fails the test if any unexpected interaction takes place. People often call those Strict Mocks, but according to the book, that's a Mock. If the Test Double only records what happens, so that you can later query it to verify whether some interaction took place, it's closer to being a Test Spy.
Whether you call it a Mock or a Spy, you can implement verification similar to the above Verify
method in functional programming using the Writer monad.
Writer-based Spy #
I'll show you a single example in Haskell. In a previous article, you saw a simplified function to implement a restaurant reservation feature, repeated here for your convenience:
tryAccept :: Int -> Reservation -> MaybeT ReservationsProgram Int tryAccept capacity reservation = do guard =<< isReservationInFuture reservation reservations <- readReservations $ reservationDate reservation let reservedSeats = sum $ reservationQuantity <$> reservations guard $ reservedSeats + reservationQuantity reservation <= capacity create $ reservation { reservationIsAccepted = True }
This function runs in the MaybeT
monad, so the two guard
functions could easily prevent if from running 'to completion'. In the happy path, though, execution should reach 'the end' of the function and call the create
function.
In order to test this happy path, you'll need to not only run a test-specific interpreter over the ReservationsProgram
free monad, you should also verify that reservationIsAccepted
is True
.
You can do this using the Writer
monad to implement a Test Spy:
testProperty "tryAccept, happy path" $ \ (NonNegative i) (fmap getReservation -> reservations) (ArbReservation reservation) expected -> let spy (IsReservationInFuture _ next) = return $ next True spy (ReadReservations _ next) = return $ next reservations spy (Create r next) = tell [r] >> return (next expected) reservedSeats = sum $ reservationQuantity <$> reservations capacity = reservedSeats + reservationQuantity reservation + i (actual, observedReservations) = runWriter $ foldFreeT spy $ runMaybeT $ tryAccept capacity reservation in Just expected == actual && [True] == (reservationIsAccepted <$> observedReservations)
This test is an inlined QuickCheck-based property. The entire source code is available on GitHub.
Notice the spy
function. As the name implies, it's the Test Spy for the test. Its full type is:
spy :: Monad m => ReservationsInstruction a -> WriterT [Reservation] m a
This is a function that, for a given ReservationsInstruction
value returns a WriterT
value where the type of data being written is [Reservation]
. The function only writes to the writer context in one of the three cases: the Create
case. The Create
case carries with it a Reservation
value here named r
. Before returning the next
step in interpreting the free monad, the spy
function calls tell
, thereby writing a singleton list of [r]
to the writer context.
In the Act phase of the test, it calls the tryAccept
function and proceeds to interpret the result, which is a MaybeT ReservationsProgram Int
value. Calling runMaybeT
produces a ReservationsProgram (Maybe Int)
, which you can then interpret with foldFreeT spy
. This returns a Writer [Reservation] (Maybe Int)
, which you can finally run with runWriter
to get a (Maybe Int, [Reservation])
tuple. Thus, actual
is a Maybe Int
value, and observedReservations
is a [Reservation]
value - the reservation that was written by spy
using tell
.
The Assert phase of the test is a Boolean expression that checks that actual
is as expected, and that reservationIsAccepted
of the observed reservation is True
.
It takes a little while to make the pieces of the puzzle fit, but it's basically just standard Haskell library functions clicked together.
Summary #
People sometimes ask me: How do Mocks and Stubs work in functional programming?
In general, my answer is that you don't need Mocks and Stubs because when functions are pure, you don't need to to test interactions. Sooner or later, though, you may run into higher-level interactions, even if they're pure interactions, and you'll likely want to unit test those.
In a previous article you saw how to apply state-based testing in Haskell, using the State monad. In this article you've seen how you can create ad-hoc Mocks or Spies with the Writer monad. No auto-magical test-specific 'isolation frameworks' are required.