Types and tests are not at war. Choose both.
In fact, if we tilt our heads a bit, types are just another flavor of test.
Don’t use just one flavor of testing; use all the tools you have at your disposal to make the best software you can.
Robert C. Martin believes code TDD’d into existence, and so having 100% test coverage by construction, nullifies the value of types:
My own prediction is that TDD is the deciding factor. You don’t need static type checking if you have 100% unit test coverage. And, as we have repeatedly seen, unit test coverage close to 100% can, and is, being achieved. What’s more, the benefits of that achievement are enormous.
Therefore, I predict, that as TDD becomes ever more accepted as a necessary professional discipline, dynamic languages will become the preferred languages. The Smalltalkers will, eventually, win. (Robert C. Martin, “Type Wars”, 2016)
The further your own development practice is from TDD, the more ludicrous this will seem to you.
If you ignore Martin’s emphasis on TDD, and focus instead on the “100% unit test coverage” bit, you’re likely to reject it out of hand: Coverage measures are a very fraught and limited measure. Even if you go “but that’s just line coverage!", well, not even 100% branch coverage suffices to demonstrate freedom from fairly mechanical bugs, never mind more abstract errors in how you’ve implemented whatever half-imagined, unspecified system you’re aiming at.
The Compilation Test
I think he’s leaving a tool on the table, though. Not even any tool: A robust bevy of tests. And a tool that slots neatly into test-driven development.
The more powerful your type system, the more oomph you can get out of simply, “Does it compile?”
Even with Java, though, you can get rather far:
Defining types is very much like writing tests—the compiler continuously checks the types for consistency while we loop back and fix errors. Step 0[, define all the types,] is exactly like normal TDD, except we are making formal statements about the system that the compiler maintains. Could step 0 take a long time? Sure. Maybe with a sufficiently-advanced type system we never even leave step 0. With Java I’m going to hit a wall pretty fast, but not before avoiding many of the worst problems with the Money design. (Ken Fox, “More Typing, Less Testing: TDD with Static Types, Part 2”, 2014)
As that demonstrates, you can usefully incorporate types into test-driven development with thoroughly salutary effects.
It Cramps My Style
It’s true that, once you’ve got a type system, you’re constrained to writing code that fits within its constraints. Often you can ram through something that doesn’t, but it’s uncomfortable and tends to come with some at least syntactic overhead that makes it not nice to do.
TDD puts you under similar constraints, though: In order to achieve test isolation, you have to structure your software differently. You’ve narrowed your collection of possible programs from all those that can be represented in your language to only those that can be test-driven into existence and so all those that yield readily to automated testing.
Both typing and testing constrain what we can do with our code; we accept both limits because they free us to build with more confidence than we’d have without either.
Types AND Tests, Or Types ARE Tests
Whichever way you look at it, use ‘em both.
Your software will be better for it, and you’ll grow to be a better software engineer for the practice.