Sorry for the mess. I'm moving things around. It'll be better soon.

Static Typing Makes (Some) Tests Unnecessary

10th June 2021

There is a common claim that by using a statically-typed language in place of a dynamically-typed language, you save the number of tests you have to write. So I wanted to help out by showing you the tests that you might write in a dynamic language, that you don’t have to write in a statically-typed language.

To demonstrate this, let’s use the simple example that is always thrown around: a method that adds two numbers. It’s small simple and easy to understand. You probably won’t ever write this method because your language already provides this functionality, but it’s enough to demonstrate all of the tests you don’t have to write when you use a static language.

In a static language (let’s use Java) the add2 method might look like this:

public int add2(int a, int b) {
    return a + b;
}

Yes, you in the corner waiving your hand. I see you. I know Java is not the best example of a static language and all of the things one can provide. I know. But it’s well known, widely used, and it is sufficient to demonstrate the point. Don’t you have a paper to write about something you don’t understand?

In a dynamic language (let’s use Ruby) the add2 method looks like this:

def add2 a, b
  a + b
end

Here are all of the tests that you might write for the dynamic version, that you didn’t have to write for the Java version.

def test_that_add2_returns_an_integer
  assert_equal(Integer, add2(1, 2).class)
end

That’s it. That’s all of the tests you wouldn’t write when using a dynamic language instead of a static language.

The rest of the tests you have to write in both languages are the same: the simple case of adding a and b, the edge cases (around the boundaries of the size of supported numbers, for instance), that it supports negative numbers, etc. This is the only test you were saved from writing because the compiler would’ve caught that one for you.

But let’s make something clear: you shouldn’t be writing that test in a dynamic language, either. It has nothing to do with the behaviour that the software will have, and the purpose of writing tests is to demonstrate that your program behaves in the expected way.

Now, if you’re a type supremacist, I’m sure you’re frothing at the mouth right now. So I’ll try to answer the things you’re thinking about.

What if I have code that passes a string to add2?

I love this argument. Because it demonstrates a complete lack of understanding about how developing software systems works, and an even deeper misunderstanding of the purpose of automated testing, and driving the design of your system by describing the behaviour it will have.

First you need to think about why you’d ever do this. Since a lot of applications today are written for the web, you’ll deal with a lot more strings than you otherwise would. You can make every caller sanitize their data first, or you can (and your test suite will let you know if this is a problem for you) get the number you’re looking for out of the string in the first place. In Ruby, you can use to_i, to_f, to_r, and a variety of similarly named methods to get the kind of thing you’re looking for.

If your add2 method needs to accept strings as input, that’s behaviour that your add2 must accommodate, and you’ll have a test for that. Regardless of the type system you’re working with.

That’s fine in an application with a good test suite, but most test suites are bad.

This is absolutely true. But it’s incomplete.

Many test suites are bad. That’s a problem with the test suite, and represents deficiencies in the development of the entire system. If the test suite doesn’t cover all of the behaviour that the system exhibits, then it’s incomplete. And having a test suite that doesn’t exercise the behaviour of the system is irresponsible.

The minimum responsible test suite will catch this kind of error.

It might catch it slightly later in the process, but this is a tooling problem. We have tools and methods that address this, and as an added bonus they add to the reliability of your system and the joy of developing in it.

What about libraries?

Apparently we’re supposed to treat libraries differently than non-library software. I don’t understand why: if the library dictates that a string should not be passed to this method, then there should be a test that covers this. If the library dictates that you can pass a string to this method, then the test suite should cover that case too.

The same arguments apply here: the minimum responsible test suite should cover the possible use cases. The documentation (which can be generated from the test suite!) should cover this case, too. This makes your library more robust, and provides a better experience for its users.

What about nil?

Discussions with type supremacists often come back to our old friend nil and this is another case that I don’t find useful. Why are these folks passing around nil so often, and why is some other test not failing when they do so?

If you pass a nil where you didn’t expect to, your test suite will catch this. If it didn’t, your test suite is incomplete.

Static Types Enable Code Inspection and Refactoring

So many favourites to pick from! This is a strong contender.

Code inspection and refactoring are not only easy to do (easier in many systems, because the reflective capacity of dynamic languages is often built-in, and more robust) but they were invented in a dynamic language. They were later ported to other languages, but the fact remains that they were in dynamic languages first, and they were (and are!) rich and useful.

Yes, when you have substandard tools, static typing makes inspection and refactoring easier. But shouldn’t we be asking for better tools?

Conclusion

My general feeling is that a lot of the “type supremacists” don’t feel that tests are valuable, and are looking for excuses to not write them.

I think most people are treating the test suite as optional, and practise that way. They’re backfilling tests after the fact, and missing a lot of valuable cases. They’re aren’t “not writing tests that are obviated by the type checker” they’re leaving out valuable behavioural checks that demonstrate that the software does, in fact, work.

If you have a test suite, and you’re running it anyway, regardless of whether you’re using static types or dynamic types, this error will be caught. Perhaps a little later in the cycle, but how much time was saved by not having to appease the overly-strict type system on matters that don’t make any difference to the behaviour of your software?

What’s my preference? Well, I prefer Smalltalk and Lisp. In that order. But, sadly, I’m not getting paid to write Smalltalk or Lisp most of the time. So I’ll use whatever language the team is using, and adjust my designs accordingly. We, as programmers, like programming languages, and for some reason we want to pretend that our irrational and emotional choices on the matter are rational and based in reason. They aren’t.

Developing software of a high quality, that behaves as expected, can be done in many environments. But if you think static type checking prevents you from writing tests, you have not demonstrated knowledge of programming, you have demonstrated a lack of knowledge of automated testing.

Use whichever languages and tools make you happy (or whichever you’re forced to) but please stop pretending that using one kind of language means you get to do less work to prove that your software behaves the way it was intended.