Except you often need to rewrite them, so now you've got two places (per 'unit') where you could have introduced a bug. Integration tests and E2E tests are far more valuable because they're attacking it at the business logic side, which is far less volatile, and particularly in a refactor, a useful invariant.
I often feel that people take "unit" test too literal and E2E as well. You can write perfectly valid, fast and useful partly integrated tests with common unit testing frameworks.
The other thing is that like you and come siblings have pointed out, many if not most people write unit tests all wrong and in the end just test the mocks. Those are really bad and you can just throw them away. Same with all those tests that just check that the right internal calls are being made. Tests nothing.
You need to attack the "business end" of your unit (or small groups of units). Inputs in and assert the outputs. Asserting that a certain collaborator was called can still make sense but if that's literally the only thing you do it's not very valuable at all.
You can generally see whether a unit test was a good unit test based on the fact that you were able to refactor the implementation of the method _without_ having to change the unit test. Yes, those definitely do exist, even in larger systems.
> You can generally see whether a unit test was a good unit test based on the fact that you were able to refactor the implementation of the method _without_ having to change the unit test. Yes, those definitely do exist, even in larger systems.
We might be in different types of software development. There's seldom an actual "specification" to a level of detail that you could test to in the sense you're probably thinking of (but I'm having to guess here for lack of detail and context from your end).
In the field I work in for example, a detailed specification in the way I'm guessing you mean would be prohibitively expensive and just not cost effective at all vs. the benefit you can get from throwing something together from imperfect information and improving upon it iteratively.
There was a (WP?) article on HN recently about "Releaseing software the right way" (or similar title), which basically said to use whatever approach actually makes sense in your circumstances. The example IIRC was hardware development (detailed specs) vs. a SaaS company.
In terms of something like embedded systems, aerotech, etc., the specification extends all the way to the unit. In terms of an SaaS, the specifications extend all the way to business logic such as (cartoon examples):
- don't charge the customer twice
- or when I click submit on the front-end the following possibilities happen according to the back-end response
- or when the back-end receives x, the inventory should be updated according to this business logic, as well as y
Say if you're doing this capital-A Agile style, all of these should be present on the acceptance criteria for any user story. As someone who's worked in rapid applications development for mobile, I can say reaching this level of specification increases speed, and reduces redundant communication. It often doesn't take half an hour for someone to write, and then it's iterated on by the team before implementation, and during.
> I often feel that people take "unit" test too literal
Perhaps. And this is where I think a useful distinction can be made between the "unit" (usually a function, or sometimes a single file, e.g., a C-style compilation unit) and a "system" (a collection of functions that perform complementing tasks, sometimes also a unit).
> You need to attack the "business end" of your unit (or small groups of units)
It's worth noting here, that sometimes the business logic extends all the way to the "unit". This is usually in very technical domains. Like say, writing a maths helper library for consumption by other programmers (either internally or externally) would often have a clearly specified outcomes at the unit level.
> Except you often need to rewrite them, so now you've got two places (per 'unit') where you could have introduced a bug.
That's not a bad thing, though. DRY might be fine for your main implementation, but redundancy is a time-tested way of catching errors (a.k.a. "double checking").
In theory, if adequate attention is spent on both maintaining the implementation as well as the tests, this is perfectly valid. In practice, this trade-off between expedience and verification goes towards the former when it comes to rapid development.