I'd say that TDD being more popular in untyped languages speaks against TDD, as it hints that maybe some of its benefits are covered already by a type system.
I don't think it is lack of types at fault for untyped languages liking TDD (though I miss types a lot). I think it is there is no way to find out if functions exist until runtime (most allow self modifying code of some form so a static analysis can't verify without solving the halting problem). Though once you know a function exists the next step of verifying the function (or an overload in some languages) exists does need types.
I'm also against measuring coverage - I've never seen anything useful to do with the measure so why bother.
Now, proofs can be often devilishly hard to write whereas tests are easy (because they're just examples), so in practice, types probably won't supplant tests even in dependently typed languages.
Tests let you instead say "with this setup here is what happens", and then ensure that whatever else you change you don't break that one thing.
To my knowledge nobody has scaled proofs to very large problems. I still think proofs should have a place in better code, but I can't figure out how to prove anything in my real world code base. (I could probably prove my languages basic containers - something that itself would be valuable!)
No, that's a separate problem. I agree that we don't always know what we need our code to do very precisely - although I think we could still increase the number of known invariants/properties in many situations - but even when you do, a proof is often hard to write.
Proofs also typically have the problem that they're not refactoring-proof: if you change the implementation of function f (but it still does the same thing), tests won't have to change, but a proof would have to be rewritten (the type would stay the same though).
for big things wiring many things together, will use normal tests(given lean4 allows to reflect on io graph, i guess can have some fun here too)
Those are not proofs. If you have a formal proof tool (I'm not aware of one for rust, but there is a lot of progress in this area) they feed and and sometimes tools can prove things. Though beware, there are limitations to this approach - sometimes they would have to solve the halting problem to say code is correct (though if they say code is wrong they are right), other times the problem is solvable only on a computer that doesn't exist yet.
Types and const are a form of formal proof - if you don't cast those away (I'm not sure what rust allows, particularly in unsafe). However there is a lot more potential in formal proofs. Rust's borrow checker is formal proof, and there are safe things you might want to do that the borrow checker doesn't allow because if it was allowed rust could no longer prove your code memory safe (a trade off that is probably worth it in most cases)
> I'm not aware of one for rust, but there is a lot of progress in this area
https://github.com/model-checking/kani is probably the best known one, I believe there are a few others.
> (I'm not sure what rust allows, particularly in unsafe).
You can't "cast const away" in Rust, even in unsafe. That is, you can do it in unsafe, but it is always undefined behavior to do so. (I am speaking about &T and &mut T here, const T and mut T exist and you're allowed to cast between those, as they have no real aliasing requirements and are really just a lint.)
If you look at any well tested program in a dynamic language, almost all the tests check the same properties that a type system would also check by default. If you remove those, usually only a few remain that test non-trivial properties.
EDIT: And I just love that in the time I took to write this, somebody wrote a comment about how it isn't so. No, it is still blatantly obvious.
One attacks the problem of bugs from the bottom up and the other from the top down. They both have diminishing returns on investment the closer they get to overlapping on covering the same types of bug.
The haskell bros who think tests dont do anything useful because "a good type system covers all bugs" themselves havent really delivered anything useful.
Please don't do this. It's not constructive.
Throwing away the test means throwing away that user story and the value that comes with it.
Mine is that a lot of potential errors (typos, type mismatches) don't need to be exercised by running code in typed language.
Yours is... well, you don't really address it.
If you're not, that suggests you're not doing them right which in turn suggests why you might have an issue with them...
I suppose you could do something like, enumerate every possible combination of inputs and check that some property holds for all of them. Or, maybe you could instead randomly select a number of combinations of inputs and check that a property holds for each of those random combinations, but that wouldn't be guaranteed to find the inputs for which the specification isn't satisfied.
I guess maybe if the test passed to the function to be tested, mock values, such that the function is effectively evaluated symbolically (where any branching that depends on the inputs to the function, would maybe have the mocked object specify what the result of the conditional should be, with different tests for different cases?) ?
Or.. Can you explain how you write tests such that they truly function as specifications?
You should be able to find a property testing library in your favourite language such as Hypothesis (python), Quickcheck (Haskell), Fastcheck (JS/typescript), etc.
Where X, Y and Z are very specific.
These example scenarios work well as a communication medium for discussing intended program behavior as well as translating well into tests.
>enumerate every possible combination
Whereas if you start doing this you will probably confuse your stakeholders.
Specific examples tend to make better specifications.
That to me doesn’t seem like what a “specification” is.
Now, maybe having good examples is more important for business use cases than having a specification.
But I generally wouldn’t call it one.
Maybe you meant to invoke .bar(), but at least we know thanks to type checks that the target exists.