Testing the update function
Before we proceed, let’s verify that our implementation of the update function works as we expect it to. We’ll do this by writing some tests.
Creating Variant Values
For the tests we need to create Variant values. To
create values of type Variant, Transit
provides the v function from
Transit.VariantUtils. 1
doorOpen :: State
doorOpen = v @"DoorOpen" {}
doorClosed :: State
doorClosed = v @"DoorClosed" {}
close :: Msg
close = v @"Close" {}
open :: Msg
open = v @"Open" {}Since having no data associated with a case is very common, the
v function has a shortcut for this: You can just omit the
empty record argument:
doorOpenShort :: State
doorOpenShort = v @"DoorOpen"Now we are well prepared to start testing the update function that we’ve implemented previously.
Testing State Transitions
To test our update function, we’ll use two functions from the
Data.Array module:
foldl :: forall a b. (b -> a -> b) -> b -> Array a -> bscanl :: forall a b. (b -> a -> b) -> b -> Array a -> Array b
The simplest way to test the update function is to use
foldl to apply a sequence of messages and check if the
final state matches what we expect:
specWalk1 :: Spec Unit
specWalk1 =
it "follows the walk and ends in expected final state" do
foldl update (v @"DoorOpen") [ v @"Close", v @"Open", v @"Close" ]
`shouldEqual`
(v @"DoorClosed")🗎 test/Examples/Door.purs L54-L59
This test starts with the door open, closes it, opens it, then closes it again. It checks that we end up with the door closed, as expected.
This test only checks the final result. To be more thorough, we
should also verify that each step along the way works correctly. The
scanl function is perfect for this — it shows us all the
intermediate states, not just the final one.
specWalk2 :: Spec Unit
specWalk2 =
it "follows the walk and visits the expected intermediate states" do
scanl update (v @"DoorOpen") [ v @"Close", v @"Open", v @"Close" ]
`shouldEqual`
[ v @"DoorClosed", v @"DoorOpen", v @"DoorClosed" ]🗎 test/Examples/Door.purs L61-L66
This test is similar to the previous one. But instead of just checking the final result, it verifies each step along the way: after closing, the door is closed; after opening, the door is open; and after closing again, the door remains closed. This makes sure each transition works correctly.
More
ergonomic testing with assertWalk helper function
Since we’ll want to write more of these tests for further examples,
it’s helpful to define a reusable helper function. The
assertWalk function takes an update function, an initial
state, and a list of message/state pairs representing the expected walk
through the state machine:
assertWalk
:: forall msg state
. Eq state
=> Show state
=> (state -> msg -> state)
-> state
-> Array (msg /\ state)
-> Aff Unit
assertWalk updateFn initState walk = do
let
msgs :: Array msg
msgs = map fst walk
expectedStates :: Array state
expectedStates = map snd walk
actualStates :: Array state
actualStates = scanl updateFn initState msgs
actualStates `shouldEqual` expectedStates🗎 test/Examples/Common.purs L40-L59
The function extracts the messages from the pairs, applies them
sequentially using scanl, and verifies that the resulting
states match the expected ones. Here’s how we use it:
specWalk3 :: Spec Unit
specWalk3 =
it "follows the walk and visits the expected intermediate states" do
assertWalk update
(v @"DoorOpen")
[ v @"Close" ~> v @"DoorClosed"
, v @"Open" ~> v @"DoorOpen"
, v @"Close" ~> v @"DoorClosed"
, v @"Close" ~> v @"DoorClosed"
, v @"Open" ~> v @"DoorOpen"
, v @"Open" ~> v @"DoorOpen"
, v @"Open" ~> v @"DoorOpen"
]🗎 test/Examples/Door.purs L68-L80
The ~> operator is an infix alias for
Tuple. So v @"Close" ~> v @"DoorClosed" is
equivalent to Tuple (v @"Close") (v @"DoorClosed").
We read it like: Starting from state DoorOpen, when
receiving message Close, we expect the next state to be
DoorClosed. From there, when receiving message
Open, we expect the next state to be DoorOpen.
And so on.
It’s a convenience wrapper around
Variant’sinjfunction that uses type application (no Proxy needed) and allows omitting empty record arguments.↩︎