> The problem with this is that this is testing the “tip of the iceberg” that will be almost the whole, final app.

This is also what I've run into in practice, across numerous systems.

Actually the only cases where I've been able to do TDD effectively (and get >95% test coverage) was when I was in control over the entire codebase and its design from day one, thus being able to enforce whatever modularization was necessary by the tests, otherwise the systems quickly tended to get untestable (at least in regards to being able to start with a test). In addition, I was generally more successful in cases where I was dealing with writing libraries (that are called by other code), as opposed to web applications which need to deal with shuffling around bunches of data.

Let me give you a few examples of what didn't work. Suppose that I'm asked to help out with this pre-existing codebase that's been around for 5 years or so. I look how some request to purchase a product is processed (just a made up example, though along the lines of what I've seen): first you have some server side rendering with the full complexity of PrimeFaces and around 10-20 variables that affect whether certain buttons are visible, possibly there being a modal dialog or redirection in the middle of the process.

Then, you have the view code, which once again contains different methods that interact with services, passing data around and choosing whether to display additional messages or something else. Then, in the service layer, you might have 5-10 services that are involved in retrieving the data necessary for everything, from auditing and e-mail notifications, getting various pieces of information about users/billing/discounts and then checking all of that against what the user actually wants to purchase, perhaps some validation code thrown in along the way.

Validation code which also might need to interact with the database due to incrementally grown business requirements over the years. And from there on out, additional code for returning early in the case of any errors being present, which might need to be further processed for displaying them to the user. Oh, and if the process is successful without any errors, then you might also have some scheduled processes that would be created and would need to be handled. Almost any of these steps could have transaction rollbacks along the way and there might also be a large amount of PrimeFaces/Spring related code and workarounds in there.

And I've seen similar patterns across many projects, almost regardless of technologies used (Java is just a good example): if you give developers the chance to create tightly coupled code, they will jump at it horrifyingly often, especially if they're under time pressure or have a "good enough" mentality. Over time, any sorts of boundaries will dissolve, unless you go for a microservices based approach, but even then you'll still be dealing with "magic" code which will be hard to test (especially if you need some kind of application context to be running during the tests), be it IoC/DI, annotations/decorators, proxies for your code, or even something like ORM interactions.

Speaking of databases, good luck if your code has like 10-20 database calls along the hot path, which you'll need to mock or actually let it hit some database instance, though hopefully one with state that's either reproducible or can be rolled back (both of which will make your CI slow and more error prone). You might go for an in-memory database, but good luck trying to transpose or make Oracle-specific complex queries portable to something like that, given that for many learning JPQL is too much.

In short: there are situations where TDD is simply unsuitable because of how certain systems/domains are architected (I'm yet to see the PrimeFaces layer as something easily testable) and you're better off simply using unit tests where you can, as well as a healthy helping of integration tests (which you may or may not be able to do, depending on how your environments are laid out).

Of course, things might not look so dire for projects that are a bit newer and where you can still alter their structure. For library code, however, personally I'd recommend TDD, especially if you're ever in the need of doing anything security adjacent (e.g. wrapper code around proven security libraries). And for basically all of the cases where you can write mostly functional code with little to no side effects.

Also, here's a not so fun memory: trying to write tests that dealt with loading data from the file system. In Java, you can have a common piece of code for doing that (e.g. Paths.get), though what paths are valid will depend on the actual system and file system that JVM will be running on. In other words, when locally you have Windows and the CI server has a Linux distro or something, your tests will not run consistently. But you don't want to test that bit of code? Better be prepared to add it as an exception to any code coverage quality gate that you've set up. So annoying.