Types are the best tests
Code constraints, I mean. They are very similar in my opinion, so this is just 1% clickbait.
I’ve always preferred statically-typed languages (C#, Golang, Typescript) over dynamic languages (Python, Ruby, Javascript). The reason is because I hate testing. That’s just my intuitive preference which I will try to explain below. For starters, here is the tweet by Uncle Bob:
Static type checking means type constraints are checked on compile time, and so your program doesn’t even compile if there are type mismatches. In dynamic language it’s only checked during runtime. Now obviously getting type errors when running in production sounds pretty bad. So you can write tests to “poke” code, in practice achieving same thing as static type checker.
It is not the goal of static typing to avoid such tests, according to Uncle Bob. That’s not how he directly phrases it, it’s a bit broader, but this is one apparent implication. In a broader sense, tests and types should be orthogonal and solve different problems.
In my view, both tests and types are constraints to a program. Test is just ultimately-flexible, turing-complete, blablabla… constraint, while type is simpler, more universal constraint.
Does it make sense to completely separate these constraints and say that they are orthogonal? Here I agree, yes (trollface). Types are good constraints, and tests are bad constraints. I’m almost serious here. For most of the code, types combined with good programming practices are almost all that is required to have correct program. Provided that all the code is statically checked, which is of course not always the case, e.g., inline SQL.
I’m not even talking about languages with super powerful type systems such as Haskell, traditional type systems such as of good old C# are good enough. Trying to replace tests with types is also a mistake in my opinion, and while I don’t know much about Haskell, I have a feeling it may be too far in that direction. Though not sure, this feeling is just from a rather limited exposure to Haskell.
Are tests completely pointless, then? No, they are useful in many cases. Just not where they would be called “burden”. Having automated acceptance test to check functionality that you’re developing can often be faster than trying to constantly check it by manually navigating UI, or they can be useful as a way to invoke type checking for non statically-typed code fragments. Having unit tests for pure calculations can make sense because such tests are easy, so no burden.
Tests are almost a requirement for hipster dynamic languages. They are not as much of a benefit for static languages. Unless you’re doing some advanced stuff such as property-based testing, they tend to be very context specific: localized, having 1:1 correspondence to code, and that makes them fragile. Because they test “piece of code” rather than behavior, something wizards in ivory tower told you should be doing, but never told you how. And to write good tests will take 3x time it took to write code in the first place. Oh yeah, now that you’re asking how dare I write code before tests: TDD has no other benefit than to force you to write tests. Which again, is required for dynamic languages. I don’t believe it helps with code design as some claim. It’s the opposite. Constructing solution by small iterations can work sometimes, but it’s a sure way to arrive at local minimum other times. Better approach is to think before you jump into coding. As to making code testable: explicitly pass all dependencies to a function, its a good idea for code readability etc, not hard to do even if there are no tests forcing you. Functional programming is just a collection of static classes with static functions, remember?
If the code is complex, tests can come to help in clarifying what the code does. However, writing good tests is maybe even harder than writing good code. I’ve yet to see a good test suite where just looking at tests convinces me what system does, and I don’t want to look at the code. Most often than not, its the opposite: test descriptions don’t match actual tests, tests are cluttered with fixture setups to a point where it’s hard to get the point of a test, code is tested at random places with no indication what is important and what not so much. All because testing is hard. I’d rather be looking at clean code with no tests, than complex code with tests, or any other combination (even clean code with clean tests, because at this point tests provide no value, its all apparent in the code).
Testing is useful when you don’t know what you’re doing, for double-checking. And for others to make sense of what you’re doing. And for e-***** size aka code coverage metrics. None of which is essential if you write simple straightforward code. Except for a bit about size, which should be kept private anyways.