Our standard practice is to introduce a thin layer in front of any I/O code, so that we can mock/simulate that part in tests.
So, if your database-library has an insert()-function, you'd introduce a interface/trait with an insert()-function, which's default implementation just calls that database-library and nothing else. And then in the test, you stick your assertions behind that trait.
So, we don't actually test the interaction with outside systems most of the time, because well:
- that database-library is tested,
- the compiler ensures we're calling that library correctly (assuming no use of a scripting language), and
- it's often easier to simulate the behavior of the outside system correctly, than to set it up for each test case.
We do usually aim to get integration tests with all outside systems going, too, to ensure that we're not completely off the mark with the behavior that we're simulating, but those are then often reduced to just the happy flow.