I feel like a bit of a troll for this -- I don't mean to be -- but...
I can never truly put my finger on the exact problems I have with the TDD approach, but something always feels off about it, whenever I read an article such as yours (well-written, though it is).
Even in this small, demonstrative function, you have ultimately written an significant quantity of non-functioning code and, in all fairness, what have you actually got to show for it? A handful of boolean checks that only really test for the specific microscopic input/output validations that you were able to think of prior to coding.
Very little actual 'end-user' functionality has been provided, for possibly 3x the effort. It looks good on your lines-per-day KPI (I feel for you if you have one of these), but most of those are not furthering the project. If you've been told to implement a soundex function in C# and, at the end of the day, you have 20 failing tests and a method stub...well, that's not going to hold you in high stead.
You touch upon one thing quite nicely when you suggest that many people might find it 'unnatural'. It is jarring to jump back and forth between the method that you are trying to implement and the tests that you write (even when reading the article). I would rather concentrate on implementing the method, then concentrate on confirming that it is correct.
Not to mention seeing everything fail all the time. I certainly know that I don't like writing things that I know will fail. I know (and work with) many other developers that feel the same. Some of them (I try very hard not to lump myself in the same category but I expect, in some cases, I am included) will start writing their code purely to pass the tests, rather than really understanding why that test is there (I know, bad developers do bad development, move along, nothing to see here!). It can be particularly insidious, especially when [font="Courier New"]Assert.AreEqual('a', func('b'));[/font] is met with [font="Courier New"]if (input == 'b') return 'a';[/font], just so that red X goes away.
(That said, countering the possibility of a null or empty input parameter with two tests and a substitution that still goes through the loop, rather than a basic [font="Courier New"]if (string.IsNullOrEmpty(input)) return string.Empty;[/font] -- or similar -- at the top of the method further cements the inefficiency of the approach in my mind!)
I should point out that I'm not opposed to unit tests, automated or otherwise. I use them often. They are, as you say, particularly helpful in protecting existing code from future changes (although, not so much when requirements changes result in having to change both the functional code and the test cases). I just think that 'writing-first' and 'writing-to-fail' are not always the best approach.
In this particular case, I would be more inclined to have a single test method comprising a number of input/output assertion pairs (including null and empty inputs) -- and I'd write it immediately after the method that actually paid the bills. Not strictly in the spirit of the whole unit-test 'movement', but functional enough to provide a smoke-test for future changes, separate enough to not break concentration during the crux of the task, and typically quick enough not to irk the guy paying my wages!
PS: Oh, and, by the way...
- An IsLetter() function is generically useful and would probably sit in a centralised maintained code library rather than in an individual solution
- This implementation simply checks for capital letters as we know our inputString is being forced to upper case. A more robust implementation would also check for lower case letters.
See Char.IsLetter -- a generically useful, centrally maintained library method, robust enough to check for lower-case letters. 😉