If you have complex calculations or data transformations or you know well the expected input and output of what you're trying to do TDD actually speeds you up by allowing you to iterate faster on your solution.
If you're writing glue code between a number of other application components (90% of development in web backends) then TDD loses a lot of benefits.
It may be different in dynamic languages, but in statically compiled ones the compiler does all the testing required for glue code.
It cannot, for example, tell if you have glued things together in the wrong order.
...but a test of the glue code alone will not necessarily tell you that, and you will waste a lot of time mocking the lower-level functions to perform a standalone test on it. It is better to test an assembly as a unit, which will test the glue code (and give you a coverage count for it.) In many cases, it will also test your low-level code without you having to write so many mocks and harnesses.
I think you can find TDD zealots who insist that you have to write tests for every little piece in isolation, but this takes a lot of time, especially if you have to write a lot of mocks to do it (and testing with mocks is error-prone; the mocks may conform to your invalid assumptions.) There is never enough time to test everything, so excessive unit testing means giving up some potentially more informative integration tests. As in most aspects of development, good judgment matters more than strict obeisance to the rules.
But I am not everybody, and trying to set team policy on my comfort levels is quite likely a bit dangerous.
I've paired with a few smart people this month and was horrified by how they refactor. Most trust their hands more than the tools. They do a LOT of transcription instead of copy and paste, and don't use the IDE's refactoring tools at all. That's not just inviting human error to the table, that's giving it the seat of honor.
For most processes there are ways to do it that are worse than nothing at all, and unfortunately many of us seem drawn to them almost instinctively. I try not to think about it because it just makes me angry that the distance between what we collectively know and what we individually do is so wide.
Transcription makes you think about what you're typing in and avoids the whole class of errors that arise from copy-and-paste. Transcribing code rather than pasting it is not a red flag.
The big problem is that you're chewing up short term memory slots that used to hold the next thing you planned to do. For me I generally use only a couple slots for a single refactor because it's all muscle memory.
Additionally, it just takes a hell of a long time, which means you go from virtually no chance of interruption to a measurable one.
When I was young I had an exceptional short term memory and could pull off some crazy big refactor without introducing errors. But now it's normal at best, and also I rely far less on those sorts of feats, in part because I put a higher value on commit history readability.
Funnily enough, all of those are also problems with copy-and-paste. But they're much more likely to occur through copy-and-paste, because it's easy to do that without reviewing the code.
I have the same issue you have. I luckily work in a language with great refactoring tools, but it's like pulling teeth to get people to use them rather than cut and paste.
Could be they've forgotten how, but this isn't the first experience like this. I am doing fine with the tools even though they don't do everything when the language is not statically typed).
Java isn't perfect and when people also treat their IDE as a glorified text editor I think it is no wonder they dislike it.
(If you go with Java, embrace it. This is probably the thing I disliked most about the way programming was taught in school: they used Java, but without the tooling that makes it awesome. Supposedly so we should learn it well.
If anyone here teaches Java: by all mean have your students use javac in the first assignmemt but teach them maven the very next thing you do. Learn them to use an IDE - it's not harmful as long as you don't accept autogenerated code.)
The part in parenthesis is just a rant about education and how teachers use notepad and javac to teach Java.
So my point is the only thing students shouldn't use in an IDE is code generation.
If I had been given a zip with a basic pom file and the correct main and test directories I think I'd had a much simpler start.
It's possibly the single tool[0] I'll miss from the .Net world when I program in Java.
(For anyone who might be wondering why not Resharper that is because that functionality is provided out-of-the-box with all major Java IDEs. On .Net however, Resharper seems to be almost a must-have for many.)
[0]: I'll be missing Asp.Net MVC, parts of Entity Framework and parts of the C# language too.
For instance, if you were working on a big app and there was a bug, writing a failing test for that bug and then looking into the fix is very helpful. But if you're starting a new project from scratch with loose requirements, or worse yet, building a prototype, starting with tests would be a waste of time at best.
Well, first of all, that's not TDD. TDD is Test-Driven Development. In your scenario, you wrote the test after the code in question. In TDD, the whole point is to write the test before the code in question.
So the value in your scenario is only insofar as the test is a useful way to quickly exercise the code in question. For example, if we have some sort of customer-facing website and there is a bug in how they save their user profile, it will be easier to test fixes to the problem if we don't have to go through the full flow of logging into the site, navigating to the profile, making edits, and clicking save all the time. But having an XUnit-style framework for running that test is not necessary for that.
It's a regression test, which is definitely TDD. The code in question is the fix for the bug, the test verifies that the bug is fixed and will remain fixed.
So any new changes/bugfixes you will make to this legacy codebase, first you will write a test and then make change. That's pretty standard. Many legacy codebases will have no tests and people who wrote the code will have left long time ago.
It's adopting test driven development during maintenance of an existing system. It's exactly TDD (identify need, write failing test, code to pass test.) TDD is a discipline that can be adopted at any time, not an SDLC that can only sensibly be adopted at the initiation of a project.
There's a bug, you write a test, "this bug shouldn't exist", that test currently fails, then you write just enough code to fix the test, then refactor. That's TDD.
First I sketch a rough implementation design on a napkin or whiteboard. I don't worry about making it perfect, just good enough to create an initial mental model, and perhaps discuss it with someone.
Then, I write a test to express how I would like to interact with a small area of an implementation of this design, typically by calling a function or method and expressing a desired outcome.
I use the feedback from the test to drive out some simple code that resembles the design I sketched. If at any point this becomes hard, I take that as a clue that my napkin design can be improved, so I go back and tweak it.
Then I start over again. This is a quick cycle, typically 10 minutes or so. I get constant feedback on my design as well as my implementation.
I don't design code with TDD. I design code exactly the same way people who don't to TDD do it. Through intuition and experience. This is a skill, not a process. I only write tests to validate my design (not my implementation). Of course, because they are tests, they automatically validate my code as well, but that is not why I am doing it. If I only cared about testing, I might as well write them last.
More often than not my designs are imperfect, but by doing TDD I always improve them significantly, with relatively little time and effort.
It's taken me 15 years to get decent at it, and I'm still learning new techniques and abandoning others. There are many tricks to be learned. I usually only TDD when I care about the design. Quite often I don't, and then I just hack. I'm simply making a choice between going very fast now or going reasonably fast in a year. I haven't found a way to do both yet.
Sure. If you actually do it.
The problem with most companies, and the reason why I think that TDD 'works' is that it forces you to right some tests, any tests at all.
Many people, if they don't write the test first, will just end up not making any tests at all.
One team adopted TDD and forced themselves to do it for a year. They admitted it slowed their overall dev-cycle down a ton, but they ended up learning a lot about how to build a well designed API. I think more internal documentation about good design emerged from that then them evangelizing TDD.
Equally anecdotally, that was the most efficient project management I have ever seen at my workplace.
Economically, our branch office was shuttered, and all employees laid off, just a few months after we delivered it, for almost entirely unrelated reasons. As it turns out, the only reason we did the TDD project at all is because our primary contract had not been renewed, and it was a lower-value contract. After it was done, we didn't have enough work left to do. But more importantly, the main office didn't have enough work to do, so they slurped up our dregs, kicked us to the curb, and sold themselves to a bigger company.
Perhaps the lead dev saw the writing on the wall, and wanted to make a showpiece to boost everyone's resumes. Perhaps he foresaw that one of the several companies we might end up with after the diaspora might get maintenance responsibilities, and that person shouldn't hate their former team for writing crappy, unmaintainable code. Doesn't matter. Did it once, and it turned out great.
I still get nostalgic for it sometimes, when I am working in the sloppy mess I am currently assigned to.
> I absolutely think _tests_ are useful, but have never found any advantages to test-DRIVEN-development (test-first).
Seconded. Especially the crap about "make the test fail first" (making the test fail is useful, especially if you're unfamiliar with some aspect of it), but it should not be an obligation
Sometimes I write code to fix a bug, then a test the tests my bugfix. Test passes. I'm done right? Well, I make sure my test fails without the bugfix. About half the time, the test still passes. ie I wasn't testing what I thought I was testing. That's why you make the test fail.
And yes, I've seen this happen as well, either you're not testing the bug-causing issue or you're using a "Magical Mock Library" where x.does_thing() just returns true and you thought it did the thing but it didn't.
It's about the fact that it's not implemented yet, so of course it going to fail!
I somewhat disagree with the statement he makes, "we know that most developers do not have great design/refactoring skills". I've certainly worked at places that all but ordered me to never refactor code, and I suspect my experience is very far from unique. They thought "refactoring" was a made-up word that programmers used to cover up dicking around and wasting time. From a non-programmer's perspective, all they see is the programmer spending several hours with the end result being that nothing has visibly changed. Mind you, no visible changes is the ideal case for refactoring, so you have no easy means to explain why that is wrong.
He says that TDD only works if the programmer is good, but I think it's more the case that test-first only works in cases where we have very well-defined problems. For these TDD trainers that he's talking about, they've gone through the same problem set over and over again. For an experienced programmer, if you're encountering the same sorts of problems, you know what the pitfalls are and can look ahead, even if you aren't violating YAGNI, you can still do yourself some favors.
To put it to a specific metric, if you have a specification from which you're working, you basically have an answer sheet for your code. You can then write code that automatically verifies that your code matches that answer sheet. The more like "having a spec" your problem is, the more likely TDD will work very well for you.
But I don't do that kind of work very frequently. Most of my work is extremely experimental, fluid, and in a lot of ways, its behavior is arbitrary. Does a padding of 5px or 10px look better? Should the surface of a polygon be more or less smooth? Should running over the ground be 3m/s or 5m/s? Should the user have two hand tools that are identical, or should they be different tools? A lot of it has to be visually verified, because it would otherwise require a computer vision algorithm of some kind to know that the code I wrote to go from text to screen pixels worked correctly.
It's much more design than development, just that design is expressed in code rather than in Photoshop PSDs.
As a result, I focus much more on REPLs, saved REPL sessions, demo code, and making the iteration time between code change and test run as narrow as possible. What I think TDD provides (poorly) for people in this situation is this REPL-like behavior, in languages that don't particularly provide a good REPL (let us consider RoR a separate "language" from Ruby, in this case, as the expectations tend to be completely separate).
So if I'm writing an implementation of a rope data structure as part of a syntax highlighting text editor that renders into a WebGL texture, yeah, I have tests for that. Ropes and text editors are well-understood data structures, there's lots of information available about them, and there are right and wrong answers to what operations each should have and how they should behave. But that's not where the work ends. I still need to figure out how to let people interact with that text editor, be it with touch gestures or motion controllers or a keyboard or a gamepad or what have you, and that has no easy-mode.
I'm pretty much in agreement with everything you wrote here, but I want reply to one thing you mentioned...
You somewhat disagreed with, "we know that most developers do not have great design/refactoring skills", because developers may have little chance to refactor. Totally on board with this; lack of understanding of technical debt and how it impacts a team's achievement is one of the biggest problems with software management.
But my point - which I clearly didn't make well enough - was that refactoring and design skills are really the same thing, and I've seen enough modified code to understand what the design skills of most developers are.
The thing is, many refactoring efforts make code worse, not better. Refuctoring is something I've observed many times over, by people who consider themselves quite skilled at the art. The end result of their efforts is that the code is fancier or trickier, not more robust or easier to understand.
So, from a manager's perspective, how are you to discern between a programmer whose refactoring efforts save time down the road, and one whose efforts lose time down the road. Of course, normally you have technical leadership that can make this distinction and makes sure those people get a cold hard reality check, but many organizations discourage technical people from gaining enough power that they can stop other technical people in their tracks before they mess things up.
Just put it in the estimate for that new feature. When your boss starts to needle you about why such a "simple" feature is taking so long, that is when you throw the bad code under the bus. You get to demonstrate a real need, rather than just a theoretical one.
The older I get, the more I believe "Big Bang" code is a bad idea, no matter what the bang actually entails. Gotta make a branch to make a feature? Gotta rewrite a large part of the underpinnings of the app to make things able to move forward? Please don't, please develop it in parallel in the mainline.
In a way, refactoring on its own violates TDD. You are supposed to write failing tests first, then write code that passes the test. Refactoring as it is described by the most zealous of TDD advocates is just a hand-waiving over writing code without testing. If we're being strict about it, write a test that proves the code needs to be changed, then you can change the code. If we're not being strict about it, what is even the point?
Regression is some of the biggest harm that can come to users, because it violates their ingrained expectations. Greenfield defects might prevent the adoption of a new feature, in which case the status quo is maintained, but regression is forward progress that is physically lost.
Out of inspiration from Python docstrings, I have been occasionally working on a JavaScript structured documentation format that I call Pliny: https://github.com/capnmidnight/pliny
It's all about creating a database of documentation that can be rendered to arbitrary formats. The documentation pages on the website for my big project are all completely generated by Pliny.
I made it executable code rather than comments because I could iterate faster on the idea if there were a simple syntax to construct the database structure at the same time as writing the documentation, as well as by including the right setup script, I could have the database live-queryable in the REPL without any additional processing step.
Unfortunately, there's nothing to verify that the documentation matches the code. There is a lot of type information there that would be super useful to exploit to statically analyze the code, but that'd be a big project to add that. It'd almost be easier to convert all of the code to TypeScript and recreate Pliny to extend a generated .d.ts typings file, so the doc gen could use just the typings file as the database.
You have to remember there a lot programmers out there, just implementing business rules into code.
If I took on a project with low quality and had a limited budget to ensure QA - I'd prioritize E2E testing over unit testing.
In other words, people (for example web-devs) who don't write tests first tend to "test" their code by clicking around in the browser to see whether they get the desired effect.
Writing tests, running them and then iteratively updating/fixing the code is IMO so much more efficient than manually clicking around to test one's code.
TDD by started by with low level tests is just backwards. Outside-in helps you clarify the design from the perspective of the user.
It addresses a different concern than unit testing, however.
I absolutely think _tests_ are useful, but have never found any advantages to test-DRIVEN-development (test-first).
But part of that is probably my style of problem solving. I consider it similar to sketching and doodling with code until a solution that "feels right" emerges. TDD severely slows that down, in my experience, with little benefit.
What I've found works really well is independently writing tests afterwards to really test your assumptions.