Adventures in HttpContext All the stuff after 'Hello, World'

Testing Akka’s FSM: Using setState for unit testing

I wrote about Akka’s Finite State Machine as a way to model a process. One thing I didn’t discuss was testing an FSM. Akka has great testing support and FSM’s can easily be tested using the TestFSMRef class.

An FSM is defined by its states and the data stored between those states. For each state in the machine you can match on both an incoming message and current state data. Our previous example modeled a process to check data integrity across two systems. We’ll continue that example by adding tests to ensure the FSM is working correctly. These should have been before we developed the FSM but late tests are (arguably) better than no tests.

It’s important to test combinations of messages against various states and data. You don’t want to be in a position to run through a state machine to the state you want for every test. Luckily, there’s a handy setState method to explicitly set the current state and data of the FSM. This lets you “fast forward” the FSM to the exact state you want to test against.

Let’s say we want to test a DataRetrieved message in the PendingComparison state. We also want to test this message against various Data combinations. We can set this state explicitly:

"The ComparisonEngine" - {
  "in the PendingComparison state" - {
    "when a DataRetrieved message arrives from the old system" - {
      "stays in PendingComparison with updated data when no other data is present" in {
        val fsm = TestFSMRef(new ComparisonEngine())
        
        //set our initial state with setState
        fsm.setState(PendingComparison, ComparisonStatus("someid", None, None))

        fsm ! DataRetrieved("oldSystem", "oldData")

        fsm.stateName should be (PendingComparison)
        fsm.stateData should be (ComparisonStatus("someid", Some("oldData"), None))
      }
    }
  }
}

It may be tempting to send more messages to continue verifying the FSM is working correctly. This will yield a large, unwieldy and brittle test. It will make refactoring difficult and make it harder to understand what should be happening.

Instead, to further test the FSM, be explicit about the current state and what should happen next. Add a test for it:

"when a DataRetrieved message arrives from the old system" - {
  "moves to AllRetrieved when data from the new system is already present" in {
    val fsm = TestFSMRef(new ComparisonEngine())
        
    //set our initial state with setState
    fsm.setState(PendingComparison, ComparisonStatus("someid", None, Some("newData")))

    fsm ! DataRetrieved("oldSystem", "oldData")
    fsm.stateName should be (AllRetrieved)
    fsm.stateData should be (ComparisonStatus("someid", Some("oldData"), Some("newData")))
  }
}

By mixing in ImplicitSender or using TestProbes we can also verify messages the FSM should be sending in response to incoming messages.

Testing is an essential part of developing applications. Unit tests should be explicit and granular. For higher level orchestration integration tests, taking a black-box approach, provide ways to oversee entire processes. Don’t let your code become too unwieldy to manage: use the tools at your disposal and good coding practices to stay lean. Akka’s FSM provides ways of programming transitional behavior over time and Akka’s FSM Testkit support provides a way of ensuring that code works over time.