Btw in functional languages catching exceptions is not idiomatic, it breaks the functional approach.
In this example you are not supposed to test undoing the file. Undoing (deleting) is what I/O does and this part is tested by someone else, somewhere else. You only need to test the arguments that are passed to that specific I/O function. If they are correct your program is correct. If they are hardcoded you shouldn't be testing them at all.
You may want to write a log. Again, test what you are about to pass to an I/O function. Don't test if the file is actually written. If you supply the I/O function with the correct arguments and the function fails to write the file, the issue is not in your code - search for it elsewhere.
Your approach seems to be "the IO happens somewhere else". I'm not sure that that's much more of a compliment.
In OOP everything happens somewhere else because anything can hold a reference to the variable and change its value right under your nose.
To me at least these are two different concepts.
I mean, somebody has to be the "somebody else". What if it's you? Now you have to properly test the I/O code. (If you can get away with it never being you, that's fine, for you. Other people might not have that freedom, though.)
> In OOP everything happens somewhere else because anything can hold a reference to the variable and change its value right under your nose.
You're referring to a variable changing when it shouldn't. I don't think that's what the quote means. In OOP everything happens somewhere else because there's always another layer of indirection, and so the code that should change the variable isn't in the function (or file) that you're reading. This is why I say it's similar to your approach - the I/O is never in the function that you're reading. It still has to be somewhere, though. And the same thing that OOP does to the details of the computation (move it somewhere else), your approach does to the details of the I/O - it's not where you're looking, it's scattered somewhere else. For the same reason people complain about the effect of OOP in this regard, I distrust your approach for the I/O.
In the context of the article I/O is someone else's code. If it were my code it should be tested elsewhere, as if someone else wrote it. Not in every place that my application uses it.
In the particular case with writing the actual I/O code - I don't have enough experience, so I can't tell you. Maybe tests are absolutely necessary. Maybe mocks are the only way. I don't know.
What I do know is that programs transform input to output - input -> transformations -> output. You never care where the input comes from nor where the result of your transformations go. All you care is if your transformations are correct.
Your program may be very complex, with many inputs that you don't control, scattered around your logic. So prior to writing tests you have to decouple those dependencies from your logic and only then proceed with writing tests. If you are using functional language you may reduce every piece of logic to the simplest, smallest function possible, test it in the REPL, document it and move on without actually writing tests.
In this specific example, if you write the first file, but fail the second, you might want to undo the write to the first file. Or write another file to say you failed. Or write to a log. These sorts of things are important, and mocks can be a good way to fake it.