Another situation: when I play chess online I'm interacting with people. You can argue they're persuading me about how best to play chess, but that isn't their motive nor is there a set conclusion they're trying to make me reach
You should narrow persuasion from "interaction" to a kind of conversation (such as those found in HN threads), otherwise you could start arguing all information inputs are persuasion regardless of whether the source is human
This is a smart way to think of testing and will discover bugs when your understanding of the code is the most lucid.
But it does require some nuance. Unfortunately, some people think writing tests or test harnesses are wasteful because they think of shops that insist on 90%+ coverage and have test suites that are easily 5-10x the size of the application's codebase.... so they decide to forgo them completely. When in reality, the answer is probably something like: you have at least 25% of your codebase that would greatly benefit from testing, even if they're just "smoke tests"
> in order to write effective tests, a programmer had to know all of the ways that a piece of software could fail in order to write tests for those cases
No.
In order to write effective tests, a programmer has to think of the piece of software's entire input domain, carve it up into a set of equivalence classes, and then determine what the expected behavior should be for a piece of input from each of those classes. With more careful testing at their boundaries, so that you can root out edge and corner cases.
The results of those tests will then find the ways your software can fail for you.
Thinking about it this way, it starts to become clear that a huge part of your job is finding ways to simplify that input space as much as possible. Simplifying at this stage makes getting it right so much easier. The fewer special, edge, and corner cases you allow in the first place, the fewer you have to code for, the fewer you have to test for, the fewer you might accidentally miss, and the fewer others might accidentally run afoul of.
This, incidentally, is the key reason why it's good to take some time to define all your tests before you start writing your implementation. I'm not necessarily a huge advocate for "red green refactor", but I do think that at least identifying all your test cases before you ever start implementing can potentially save you boatloads of time, by helping you recognize opportunities to simplify your design before you get locked into an unnecessarily complicated one by a couple hours or days (or months) of sunk cost.
It's also the real reason (IMO) why functional programming - as a style, not a kind of language - is such a valuable discipline. The challenge with programming in an imperative style is that it turns your module's entire past history into one of the inputs you need to consider, and that turns your test and specification surface into something that is just so much bigger. Considering that it opens up the possibility of injecting bugs into a routine without ever actually editing the routine itself, or even any of the functions it calls, it's possibly even fair to say that it's unbounded.
Another situation: when I play chess online I'm interacting with people. You can argue they're persuading me about how best to play chess, but that isn't their motive nor is there a set conclusion they're trying to make me reach
You should narrow persuasion from "interaction" to a kind of conversation (such as those found in HN threads), otherwise you could start arguing all information inputs are persuasion regardless of whether the source is human