My general strategy for testing is to separate out pure from non-pure functions.
All programs need a main function which should have the context needed to run the program as it's expected to run. This code can be tested using integration tests, but can't be unit tested. It is ideal to keep this function small, using external configuration if possible.
Pure functions don't need any tricks to test correctly. Just give them data, and they'll do their thing. This code should be where the business logic resides.
Non-pure functions are necessary for performing IO tasks or for doing something which varies (getting the current day of the week for instance). These functions can sometimes be tested with integration tests, other times not. The idea is to keep the code inside these functions extremely small so that it can be easily verified with a quick glance. These functions shouldn't contain branching logic, loops, or anything of the sort.
It is ideal to perform all of the needed IO with non-pure functio