The habit that I've repeated on my last few project is this:
Work out a way to gracefully serialize/deserialize data structures in your code into a human readable format, like EDN or pretty printed JSON. (this is easier in more civilized languages... -winks at Clojure-)
Pick high level functions that exercise a lot of code for testing.
Generate input data to exercise that function either by hand if it's small and tractable, or by instrumenting real runs of the program and outputting serialized copies of that data (to file or console).
Write test helpers that call the function with the input data, and depending on an environment variable, read the expected 'golden' data from disk, or if UPDATE_TEST_RESULTS=true, write the function output back to disk with a filename unique to the test, and mark the test as passed. If the test fails, print a nice readable, structural diff of the expected/actual data - e.g. python datadiff, or clojure.data/diff so the exact change is visible.
On the first run you set UPDATE_TEST_RESULTS=true and generate the expected test results. Check your 'golden' result data into git.
Now you can easily keep expected results up to date even if logic changes, and get a git diff of exactly how the data has changes.
Acknowledged that this doesn't speak to side effect riddled code that needs mocks.. and doesn't buy into the build-the-test-first style of development, but I've found it's a good return on investment for regression testing, and provides visibility into subtle logic changes, expected or otherwise, introduced by code changes.