Depending on your scope, one thing you could do is replace the things
that operate against IO with functions that operate against some other
interface, and then provide an IO-based implementation and a pure
implementation of that interface.
That works best where the IO activity is fairly simple; of course,
when that's true you can often find ways to factor it out and just get
yourself a pure function to test anyway. It works worst when you're
calling out into complicated libraries that live in IO. There may be
a sweet spot between these two where it would be a good fit.