Testing an Event Driven Design of a Resuscitation Protocol

In a previous post a described an event driven domain design to run a resuscitation protocol. I also mentioned that running this protocol in practice can have some pitfalls. One of these pitfalls is the use of a defibrillator that is required to apply a cardio conversion by delivering a shock. We have to test that this cannot ever occur.

First to deliver the shock, the defibrillator has to be charged. However, during the whole procedure it is essential to perform cardio pulmonary resuscitation (CPR, i.e. performing chest compressions along with ventilation breaths) without the least amount of interruption. The sequence looks like follows:

  • Charge the defibrillator while CPR is continuing
  • When charged perform a rhythm check
  • If the rhythm requires cardio-version, apply the shock
  • If the rhythm does not require cardio-version do not shock
  • Immediately commence with CPR
  • If no shock was delivered, discharge the defibrillator

So, it is important to do the rhythm check with the defibrillator charged and either perform cardio-version or discharge the defibrillator.

Both charging the defibrillator and forgetting to discharge the defibrillator are easily forgotten. Also note that the following sequences should NOT occur:

  • Charge when already charged
  • No cardio-version but no subsequent discharging
  • Shock and when no previous charge
  • Discharge when no previous charge

So, a lot can go wrong. And each of the above list can be viewed as a property of the events that have occurred. Using property based testing this can be quite nicely achieved.

First, we need some functions that randomly runs the resuscitation protocol to create random scenarios, i.e. random lists of events that occurred as a result of following the protocol.

let es = [ Unresponsive |> Observed ]

let run es =
	|> getCurrentProtocolBranch  Protocol.resuscitation
	|> Option.bind ((removeNonRepeatableFromBranch es) >> Some)
	|> Option.bind (getCurrentProtocolItem)
	|> Option.bind ((getCommandsFromProtocolItem es) >> Some)
	|> Option.defaultValue []

let runRandom f n =
	sprintf "--- Start run %i" n |> f

	let rand = n |> Random

	let pickRand xs =
		let c = xs |> List.length

		if c = 0 then None
			xs.[ c |> rand.Next ]
			|> Some

	let procEvs es = 
		|> run
		|> List.map (fun c ->
			match c with
			| Observe o -> Observed o
			| Intervene i -> Intervened i
		|> pickRand

	let rec run b es =
		if b then 
			sprintf "--- Finished\n\n" |> f
			match es |> procEvs with
			| Some e ->
				sprintf "Event: %A" e |> f
				[ e ] |> List.append es |> run false
			| None -> 
				run true es

	sprintf "Event: %A" (Observed Unresponsive) |> f
	[ Observed Unresponsive ] 
	|> run false

These helper functions create a random protocol run until an ending event, either signs of life or return of spontaneous circulation.

The property test uses the generated events to check the sequence of charge -> shock or charge -> discharge events.

testProperty "For all possible runs, the defibrillator" <| fun n ->
	|> testRun
	|> List.fold (fun a e ->
		if a |> fst |> not then a
			match a |> snd, e with
			// Cannot charge twice at a row
			| Some (Intervened ChargeDefib), Intervened ChargeDefib -> 
				false, None
			// Otherwise can charge
			| _, Intervened ChargeDefib -> 
				true, Some e
			// When charge can shock or decharge
			| Some (Intervened ChargeDefib), Intervened DischargeDefib 
			| Some (Intervened ChargeDefib), Intervened Shock -> 
				true, None
			// Otherwise cannot discharge or shock
			| _, Intervened DischargeDefib 
			| _, Intervened Shock -> 
				false, None
			// Otherwise nothing relevant happened
			| _, _ -> a

	) (true, None)
	|> function 
	| false, _ -> false
	| true, Some (Intervened ChargeDefib) -> false // Events end with charged but no discharge occurred
	| true, _ -> true
	|> expectIsTrue "Should always first be charged and then be discharged or used to shock"

The property test uses the FsCheck library to test the property. FsCheck library will generate random numbers n that in turn is used as a seed number in the runRandom function.

  • line 09 checks: Charge when already charged
  • line 28 checks: No cardio-version but no subsequent discharging
  • line 19 checks: Shock when no previous charge
  • line 20 checks: Discharge when no previous charge

FsCheck will try to find the minimal failing test. So, when it fails it will report for example that the test failed at n = 3.

You can then check the exact sequence of events by running:

Tests.runRandom (printfn "%s") 3

The result is that the property is tested without having to specify specific scenarios, as there can always be one more scenario that you forgot to test.

And just to be sure:

[1..10000] (* create a list with 10.0000 *)
|> List.map (fun n -> n |> runRandom ignore) (* create 10.0000 scnario's *)
|> List.distinct (* select only the unique scenario's *)
|> List.iteri (fun i es ->
	printfn "test %i" i
	|> testChargeDischarge (* for each scenario check the charge discharge rule *)

This will check 365 unique scenario’s.

Leave a Reply

Your email address will not be published. Required fields are marked *