A very powerful idea is described in Testing Monadic Code With QuickCheck[1] (or a pdf version at [2]). I think the ideas apply to non-monadic (or non IO-like monadic) code as well.
The basic idea is to generate a sequence of events as test input, like
events = [EventA, EventA, EventC, EventB]
and execute the corresponding actions (starting from an initial state). Now you can make assertions on the final state S and events that happened.
Some ideas for your case:
- If EventB happened, S must be foo
- If EventC never happened, S must be bar (note that negative event tests are very powerful)
- S must have baz=0 only if the count of EventAs is same as count of EventBs after the first EventC
- ...
You can also compare final states resulting from different event sequences:
- if E1 is a sequence with result S1, sticking EventD anywhere in E1 must also result in S1
- ...
The paper goes into details about generating a valid event sequence, if there are restrictions about in which state can a given event happen.
Section 3 & 4 are about testing that (the effect of) two given action sequences are equal, independent of previous/subsequent actions. This is useful if you don't only handle events, but can do some custom actions on your state. Like you can test that "foo >> barBaz" is always the same as "foo >> bar >> foo >> unicorn".
Section 10-12 bring an other example. Section 13 shows a way to more easily generate a valid action sequence, but only in case you have a parallel abstract (pure) implementation, which is not often the case (except algos), and maybe this is really specific to IO/ST.