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:

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.


  1. It’s a convenience wrapper around Variant’s inj function that uses type application (no Proxy needed) and allows omitting empty record arguments.↩︎

↑ Back to top