Hacker News new | ask | show | jobs
by crazygringo 261 days ago
> I find that strong typing often obviates the need for unit tests.

Can you expand? Because my experience is they are totally orthogonal.

For me, unit testing is to ensure the function's algorithm is correct. You verify add(2, 3) == 5 and add(1, 2, 3) == 6 and add(2, Null) == Null.

But you don't generally write unit tests that tests how a function behaves when you pass an unexpected type. Nobody in my experience is testing add("a", FooObject) for a function only meant to take ints or floats, to make sure it only takes ints or floats.

So they solve entirely different problems: strong typing ensures a caller provides compatible data, while unit tests ensure a callee produces correct results (not just correctly typed results) from that data. You want both, ideally.

2 comments

> Nobody in my experience is testing add("a", FooObject) for a function only meant to take ints or floats, to make sure it only takes ints or floats.

If it's a dynamically typed language and you want to be sure that your method throws an error on invalid types (rather than, say, treating the string "yes" as a boolean with a value of true), then unit tests are a good use for this.

I would argue that failing fast (for cases like that, where an input "could" be treated as the type you want, but almost certainly the caller is doing something wrong) is a positive thing.

For certain classes of dynamically typed languages the unit test serves the function that a compiler or linter would perform just by running the code on any input to ensure it can run at all. Basically since these checks are runtime instead of compile time, you have to run the code to get even basic syntactical checking. I think ruby, python and JavaScript all used to use unit tests in this way. These days static analysis tooling is much more advanced and can provide much of the same benefit without running the code, but I think this is where the idea of 100% line coverage comes from.

Strong typing certainly doesn’t remove the need for testing, but it does change what type of issues you test for. And for certain classes of boring code that just transform or move data, a single integration test can give you all the assurance you need.

That is because typing in this case is converting a semantic (run time) property into a trivial property (T/F).

It is just making a trade off that is often a reasonable pragmatic default, but if taken as an absolute truth, can lead to brittle systems stuck in concrete.

Not all problems can be reduced to decision problems, which is what a trivial property is.

For me looking at how algebraic data types depend on a sum operation that uses either tagged unions or disjoint unions is a useful lens into the limits.

Note that you can use patterns like ports and adapters which may help avoid some of that brittleness, especially with anti-corruption layers in more complex systems/orgs or where you don’t have leverage to enforce contracts.

But yes if you can reduce your problems to decision problems where you have access to both T and F via syntactic or trivial semantics you should.

But when you convert pragmatic defaults to hard requirements you might run into the edges.