I'd describe myself as a "TDD guy." Probably the type that most of the TDD-negative comments are about. I've got quite a bit of experience doing TDD in large applications, mostly web or services for consumption by a web-app and mostly Ruby and JavaScript (a bit of Java as well). Here are some of my thoughts.

- The best test that you can write is a complete system integration test. These are usually driving a browser and integrating with a database for web apps, or making HTTP requests if it's an API / headless service. They're the best because they guarantee that the system works as a whole given whatever initial setup you do.

You can totally write these tests first. And you should. It requires you to stop and think about how the system should behave at the boundaries (interaction with the user and external systems such as a database or API). Then as you start writing the code, you don't have to do a bunch of clicking in the browser, you run a test that takes 2 seconds to know if your thing worked

- The best test that you can write is also the slowest. Got some complicated flow that behaves differently in 10 different contexts? Full end-to-end tests are too slow for this. 6 months of writing tests like this and you're looking a 10+ minute test suite, _at best_.

- If the best test you can write is also the slowest, you need more tests somewhere. If you don't have those tests somewhere, then code you write today is going to break at some point and you won't know until it hits production. This sucks. This is where unit tests come in.

- Unit tests should test exactly that, the unit. That means, the behavior of one class or function. The behavior of an object I depend on is _not_ my behavior. Therefore, if I have dependencies, you should be mocking them out. It's true that this in a way tests implementation, not behavior. But thought about another way, the behavior of one unit might be to call a method on one object and pass the result to another.

Thinking about unit tests in this way and mocking collaborators prevents the issue where you make on change and break a _ton_ of tests. It also prevents you from creating a bunch of duplicate setup when your code is broken up into lots of small objects / functions. If you don't mock your dependencies, you're setting up data for something that isn't relevant until multiple levels down the dependency chain and it's not obvious why that setup is necessary.

- Sometimes things are just too complicated and mocking all the collaborators of an object just isn't worth it. These situations should be pushed as far toward to bottom of your abstraction hierarchy as possible and then you should do an integration test from that class / function down to the bottom with no mocking.

- TDD isn't the only way to have great test coverage or the only way to write well-decoupled components, but it's hard to have bad test coverage and write highly coupled components when test-driving correctly. And when you're a year or more into building an application that's a cornerstone of your business, it's going to be super valuable to have the flexibility of a well-tested, loosely-coupled application. You won't have to spend hours in manual testing to confirm that you haven't broken anything and changes will be easier to implement.