Hacker News new | ask | show | jobs
by crtc 1922 days ago
> with types you need way less unit tests

It's a misconception that developers who use dynamically typed programming languages write tests that perform tasks of a static type system. They do not write tests like this:

    assertException(() => upcase(12));
    assertException(() => upcase(true));
    assertException(() => upcase(null));
    assertException(() => upcase(new Object()));
    ...
They write tests like

    assertEquals("TEST", upcase("test"));
    assertEquals("HELLO, WORLD", upcase("hElLo, wOrLd"));
    assertEquals("BLA123", upcase("bla123"));
2 comments

Having used both dynamic and statically typed languages rather extensively, I always end up recreating some subset of the type system in the test suite for dynamically typed languages. Often to at least test that the functions correctly handle erroneous input. Taking your example, I would definitely have at least one of those assertException kind of tests, and more of them (but automatically generated) if using a property-based test system where I could say something like: any type but string should result in an exception/error result. Now, this wouldn't be exhaustive (again, sans automation), but I'd have at least one test covering this.

Those tests that you list later are "happy path" tests. We want to know that that works, but we can't rely on only that sort of test especially if the type system doesn't work with us to avoid incorrect inputs to the function.

I currently use a dynamically typed pl in my professional capacity (statically typed pl in my side projects). I write a lot of tests, and 0 of them assert the type of the flowing data.
So you have no tests that would trigger an error/exception by giving bad data? I'm not saying that I'd, necessarily, call out the type explicitly, but I would give bad data to trigger the exceptional control flow/guard which can be tantamount to specifying a type. Of course, this also depends on where the function sits. If it's an internal/private function in a module that only my own functions would call I can more safely focus on the happy path. But if it's part of the interface to a module, then I want to make sure that users of the module get proper feedback/responses, whatever the contract is (be it a result type or an exception or a default value). I mean, that's a large part of the value of testing: ensuring that the code matches the specification/contract that you present to users.
Elixir/ecto has something called schema changesets that are a very robust way of validating user input. I do test against bad values (not just types, out of range, correct type but unsupported value, etc), but only really at data ingress, and no where else.

Honestly if a sad path causes a typing error in elixir it's not the worst thing. Sentry will catch it, only the user thread crashes, and you go patch it later.

> I do test against bad values

A type is literally a description of a set of valid values. So when you say you test with bad values, then the answer is: you could use types and would not need these tests anymore.

However, the more interesting question is: is your typesystem capable of expressing your type and, if so, is it worth the effort and implications to do so.

But on a more theoretical level, OP is right: you _can_ save the tests with, given a powerful enough typesystem.

> you could use types and would not need these tests anymore.

No. These are not internal contracts, these are contracts with user input. In a statically typed language, You are still advised to write tests that your marshalling technique provides the expected error (and downstream effects) that you plan for, if say the user inputs the string "1.", For a string that should be marshalled as an integer.

> A type is literally a description of a set of valid values.

That is generally not the case. There are, for example cases where certain floating points are invalid inputs (either outside of the domain of the function or a singular points), and I don't know of a mainstream PL that lets you define subsets of the reals in their type system.

In go, or c, c++, or rust, you could have a situation where a subset of integers are valid values (perhaps you need "positive, nonzero integers", because you are going to do an integer divide at some point) and that is not an expressible set of value in that type system. Ironically, that is a scenario that IS typable in Elixir and erlang, which are dynamically typed languages.

> But on a more theoretical level, OP is right: you _can_ save the tests with, given a powerful enough typesystem.

Yes and no. The trick is building a type system simultaneously strong enough to encode the properties you want, and weak enough that it's statically decidable.

There will always be properties you can't encode in a (useful) type system.

It's a fine line to walk, really. There's an argument to be made that most of the times we don't actually need Turing-completeness, and we'd better off using only types to encode computations, but OTOH I don't really want to think about what coinductive types I must define to solve a problem that could be solved with five lines of javascript instead.

A type system can do way more than the simple things from your first code block. This is misleading in terms of what a powerful type systems can do.

Re your second code block: This can be typed with literal types, no need for tests at compile time.